luci-app-keepalived: Add LuCI for keepalived 5965/head
authorJaymin Patel <jem.patel@gmail.com>
Thu, 8 Sep 2022 04:35:13 +0000 (10:05 +0530)
committerJaymin Patel <jem.patel@gmail.com>
Sun, 23 Oct 2022 10:49:33 +0000 (16:19 +0530)
LuCI Support for Keepalived

Signed-off-by: Jaymin Patel <jem.patel@gmail.com>
15 files changed:
applications/luci-app-keepalived/Makefile [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/globals.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/ipaddress.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/overview.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/peers.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/route.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/script.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/servers.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/track_interface.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/url.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_instance.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_sync_group.js [new file with mode: 0644]
applications/luci-app-keepalived/htdocs/luci-static/resources/view/status/include/35_keepalived.js [new file with mode: 0644]
applications/luci-app-keepalived/root/usr/share/luci/menu.d/luci-app-keepalived.json [new file with mode: 0644]
applications/luci-app-keepalived/root/usr/share/rpcd/acl.d/luci-app-keepalived.json [new file with mode: 0644]

diff --git a/applications/luci-app-keepalived/Makefile b/applications/luci-app-keepalived/Makefile
new file mode 100644 (file)
index 0000000..81b0cc2
--- /dev/null
@@ -0,0 +1,18 @@
+#
+# Copyright (C) 2022 Jaymin Patel <jem.patel@gmail.com>
+#
+# This is free software, licensed under the GNU General Public License v2.
+
+include $(TOPDIR)/rules.mk
+
+PKG_LICENSE:=GPL-2.0-or-later
+PKG_MAINTAINER:=Jaymin Patel <jem.patel@gmail.com>
+
+LUCI_TITLE:=LuCI support for the Keepalived
+LUCI_DEPENDS:=+luci-base +keepalived +keepalived-sync
+LUCI_PKGARCH:=all
+
+include ../../luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
+
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/globals.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/globals.js
new file mode 100644 (file)
index 0000000..5329d33
--- /dev/null
@@ -0,0 +1,66 @@
+'use strict';
+'require view';
+'require form';
+
+return view.extend({
+       render: function() {
+               var m, s, o;
+
+               m = new form.Map('keepalived');
+
+               s = m.section(form.TypedSection, 'globals', _('Keepalived Global Settings'));
+               s.anonymous = true;
+               s.addremove = false;
+
+               o = s.option(form.Value, 'router_id', _('Router ID'),
+                       _('String identifying the machine (doesn\'t have to be hostname)'));
+               o.optional = true;
+               o.placeholder = 'OpenWrt';
+
+               o = s.option(form.Flag, 'linkbeat_use_polling', _('Link Polling'),
+                       _('Poll to detect media link failure using ETHTOOL, MII or ioctl interface otherwise uses netlink interface'));
+               o.optional = true;
+               o.default = true;
+
+               o = s.option(form.DynamicList, 'notification_email', _('Notification E-Mail'),
+                       _('EMail accounts that will receive the notification mail'));
+               o.optional = true;
+               o.placeholder = 'admin@example.com';
+
+               o = s.option(form.Value, 'notification_email_from', _('Notification E-Mail From'),
+                       _('Email to use when processing “MAIL FROM:” SMTP command'));
+               o.optional = true;
+               o.placeholder = 'admin@example.com';
+
+               o = s.option(form.Value, 'smtp_server', _('SMTP Server'),
+                       _('Server to use for sending mail notifications'));
+               o.optional = true;
+               o.placeholder = '127.0.0.1 [<PORT>]';
+
+               o = s.option(form.Value, 'smtp_connect_timeout', _('SMTP Connect Timeout'),
+                       _('Timeout in seconds for SMTP stream processing'));
+               o.optional = true;
+               o.datatype = 'uinteger';
+               o.placeholder = '30';
+
+               o = s.option(form.Value, 'vrrp_mcast_group4', _('VRRP Multicast Group 4'),
+                       _('Multicast Group to use for IPv4 VRRP adverts'));
+               o.optional = true;
+               o.datatype = 'ip4addr';
+               o.placeholder = '224.0.0.18';
+
+               o = s.option(form.Value, 'vrrp_mcast_group6', _('VRRP Multicast Group 6'),
+                       _('Multicast Group to use for IPv6 VRRP adverts'));
+               o.optional = true;
+               o.datatype = 'ip6addr';
+               o.placeholder = 'ff02::12';
+
+               o = s.option(form.Value, 'vrrp_startup_delay', _('VRRP Startup Delay'),
+                       _('Delay in seconds before VRRP instances start up after'));
+               o.optional = true;
+               o.datatype = 'float';
+               o.placeholder = '5.5';
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/ipaddress.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/ipaddress.js
new file mode 100644 (file)
index 0000000..0cdce65
--- /dev/null
@@ -0,0 +1,90 @@
+'use strict';
+'require view';
+'require ui';
+'require form';
+'require uci';
+'require tools.widgets as widgets';
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       uci.load('keepalived'),
+               ]);
+       },
+
+       renderIPAddress: function(m) {
+               var s, o;
+
+               s = m.section(form.GridSection, 'ipaddress', _('IP Addresses'),
+                       _('Addresses would be referenced into Static and Virtual IP Address of VRRP instances'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.rmempty = false;
+               o.optional = false;
+               o.placeholder = 'name';
+
+               o = s.option(form.Value, 'address', _('Address'),
+                       _('IP Address of the object'));
+               o.rmempty = false;
+               o.optional = false;
+               o.datatype = 'ipaddr';
+               o.placeholder = '192.168.1.1';
+
+               o = s.option(widgets.DeviceSelect, 'device', _('Device'),
+                       _('Device to use to assign the Address'));
+               o.optional = true;
+               o.noaliases = true;
+
+               o = s.option(form.Value, 'label_suffix', _('Virtual Device Label'),
+                       _('Creates virtual device with Label'));
+               o.datatype = 'maxlength(4)';
+               o.optional = true;
+
+               o = s.option(form.ListValue, 'scope', _('Scope'),
+                       _('Scope of the Address'));
+               o.value('site', _('Site'));
+               o.value('link', _('Link'));
+               o.value('host', _('Host'));
+               o.value('nowhere', _('No Where'));
+               o.value('global', _('Global'));
+               o.optional = true;
+       },
+
+       renderStaticIPAddress: function(m) {
+               var s, o;
+               var ipaddress;
+
+               ipaddress = uci.sections('keepalived', 'ipaddress');
+               if (ipaddress == '') {
+                       ui.addNotification(null, E('p', _('IP Addresses must be configured for Static IP List')));
+               }
+
+               s = m.section(form.GridSection, 'static_ipaddress', _('Static IP Addresses'),
+                       _('Static Addresses are not moved by vrrpd, they stay on the machine.') + '<br/>' +
+                       _('If you already have IPs on your machines and your machines can ping each other, you don\'t need this section'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.DynamicList, 'address', _('IP Address'),
+                       _('List of IP Addresses'));
+               for (var i = 0; i < ipaddress.length; i++) {
+                       o.value(ipaddress[i]['name']);
+               }
+               o.optional = true;
+       },
+
+       render: function() {
+               var m;
+
+               m = new form.Map('keepalived');
+
+               this.renderIPAddress(m);
+               this.renderStaticIPAddress(m);
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/overview.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/overview.js
new file mode 100644 (file)
index 0000000..7e261bf
--- /dev/null
@@ -0,0 +1,75 @@
+'use strict';
+'require view';
+'require form';
+'require uci';
+'require rpc';
+'require poll';
+
+var callKeepalivedStatus = rpc.declare({
+       object: 'keepalived',
+       method: 'dump',
+       expect: {  },
+});
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       uci.load('keepalived'),
+               ]);
+       },
+
+       render: function() {
+               var table =
+                       E('table', { 'class': 'table lases' }, [
+                               E('tr', { 'class': 'tr table-titles' }, [
+                                       E('th', { 'class': 'th' }, _('Name')),
+                                       E('th', { 'class': 'th' }, _('Interface')),
+                                       E('th', { 'class': 'th' }, _('Active State/State')),
+                                       E('th', { 'class': 'th' }, _('Probes Sent')),
+                                       E('th', { 'class': 'th' }, _('Probes Received')),
+                                       E('th', { 'class': 'th' }, _('Last Transition')),
+                                       E([])
+                               ])
+                       ]);
+
+               poll.add(function() {
+                       return callKeepalivedStatus().then(function(instancesInfo) {
+                               var targets = Array.isArray(instancesInfo.status) ? instancesInfo.status : [];
+                               var instances = uci.sections('keepalived', 'vrrp_instance');
+
+                               cbi_update_table(table,
+                                       targets.map(function(target) {
+                                               var state = (target.stats.become_master - target.stats.release_master) ? 'MASTER' : 'BACKUP';
+                                               if (instances != '') {
+                                                       for (var i = 0; i < instances.length; i++) {
+                                                               if (instances[i]['name'] == target.data.iname) {
+                                                                       state = state + '/' + instances[i]['state'];
+                                                                       break;
+                                                               }
+                                                       }
+                                               }
+                                               return  [ 
+                                                       target.data.iname,
+                                                       target.data.ifp_ifname,
+                                                       state,
+                                                       target.stats.advert_sent,
+                                                       target.stats.advert_rcvd,
+                                                       new Date(target.data.last_transition * 1000)
+                                               ];      
+                                       }),
+                                       E('em', _('There are no active instances'))
+                               );
+                       });
+               });
+
+               return E([
+                       E('h3', _('Keepalived Instances Status')),
+                       E('br'),
+                       table
+               ]);
+       },
+
+       handleSave: null,
+       handleSaveApply:null,
+       handleReset: null
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/peers.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/peers.js
new file mode 100644 (file)
index 0000000..059fc1d
--- /dev/null
@@ -0,0 +1,97 @@
+'use strict';
+'require view';
+'require form';
+'require rpc';
+
+return view.extend({
+       callHostHints: rpc.declare({
+               object: 'luci-rpc',
+               method: 'getHostHints',
+               expect: { '': {} }
+       }),
+
+       load: function() {
+               return Promise.all([
+                       this.callHostHints(),
+               ]);
+       },
+
+       render: function(data) {
+               var hosts = data[0];
+               var m, s, o;
+
+               m = new form.Map('keepalived');
+
+               s = m.section(form.GridSection, 'peer', _('Peers'),
+                       _('Peers can be referenced into Instances cluster and data/config synchronization'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.optional = false;
+               o.placeholder = 'name';
+
+               o = s.option(form.Value, 'address', _('Peer Address'));
+               o.optional = false;
+               o.rmempty = false;
+               o.datatype = 'ipaddr';
+               for(var mac in hosts) {
+                       if (hosts[mac]['ipaddrs'] == 'undefined') {
+                               continue;
+                       }
+                       for(var i = 0; i < hosts[mac]['ipaddrs'].length; i++) {
+                               o.value(hosts[mac]['ipaddrs'][i]);
+                       }
+               }
+
+               o = s.option(form.Flag, 'sync', _('Enable Sync'),
+                       _('Auto Synchonize Config/Data files with peer'));
+
+               o = s.option(form.ListValue, 'sync_mode', _('Sync Mode'),
+                       _('Current System should act as Sender/Receiver.') + '<br/>' +
+                       _('If peer is backup node, Current system should be sender, If peer is master current system should be receiver'));
+               o.value('send', _('Sender'));
+               o.value('receive', _('Receiver'));
+               o.default = 'send';
+               o.depends({ 'sync' : '1' });
+
+               o = s.option(form.Value, 'ssh_port', _('SSH Port'),
+                       _('If peer runs on non standard ssh port, change to correct ssh port number'));
+               o.datatype = 'port';
+               o.default = '22';
+               o.modalonly = true;
+               o.depends({ 'sync' : '1', 'sync_mode' : 'send' });
+
+               o = s.option(form.Value, 'sync_dir', _('Sync Directory'),
+                       _('Sender will send files to this location of receiver. Must be same on Master/Backup'));
+               o.default = '/usr/share/keepalived/rsync';
+               o.optional = false;
+               o.rmempty = false;
+               o.modalonly = true;
+               o.datatype = 'directory';
+               o.depends({ 'sync' : '1' });
+
+               o = s.option(form.FileUpload, 'ssh_key', _('Path to SSH Private Key'),
+                       _('Use SSH key for password less authentication, SSH Key would be used on current system'));
+               o.root_directory = '/etc/keepalived/keys';
+               o.enable_upload = true;
+               o.modalonly = true;
+               o.datatype = 'file';
+               o.depends({ 'sync' : '1', 'sync_mode' : 'send' });
+       
+               o = s.option(form.TextValue, 'ssh_pubkey', _('SSH Public Key'),
+                       _('Authorize ssh public key of peer'));
+               o.datatype = 'string';
+               o.modalonly = true;
+               o.depends({ 'sync' : '1', 'sync_mode' : 'receive' });
+
+               o = s.option(form.DynamicList, 'sync_list', _('Sync Files'),
+                       _('Additional files to synchronize, By default it synchronizes sysupgrade backup files'));
+               o.datatype = 'file';
+               o.modalonly = true;
+               o.depends({ 'sync' : '1', 'sync_mode' : 'send' });
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/route.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/route.js
new file mode 100644 (file)
index 0000000..cf2454c
--- /dev/null
@@ -0,0 +1,96 @@
+'use strict';
+'require view';
+'require ui';
+'require form';
+'require uci';
+'require tools.widgets as widgets';
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       uci.load('keepalived'),
+               ]);
+       },
+
+       renderRoute: function(m) {
+               var s, o;
+
+               s = m.section(form.GridSection, 'route', _('Routes'),
+                       _('Routes would be refereenced into Static and Virtual Routes of VRRP instances'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.optional = false;
+               o.placeholder = 'name';
+
+               o = s.option(widgets.DeviceSelect, 'device', _('Device'),
+                       _('Device to use for Routing'));
+               o.optional = true;
+               o.noaliases = true;
+
+               o = s.option(form.Value, 'address', _('Target/Destination'),
+                       _('Target IP Address of the Route'));
+               o.optional = true;
+               o.datatype = 'ipaddr';
+               o.placeholder = '192.168.1.1';
+
+               o = s.option(form.Value, 'src_addr', _('Source Address'),
+                       _('Source Address of the Route'));
+               o.optional = true;
+               o.datatype = 'ipaddr';
+               o.placeholder = '192.168.1.1';
+
+               o = s.option(form.Value, 'gateway', _('Gateway'),
+                       _('Gateway to use for the Route'));
+               o.optional = true;
+               o.datatype = 'ipaddr';
+               o.placeholder = '192.168.1.1';
+
+               o = s.option(form.Value, 'table', _('Route Table'),
+                       _('System Route Table'));
+               o.value('default', _('default'));
+               o.value('Main', _('Main'));
+               o.optional = true;
+
+               o = s.option(form.Flag, 'blackhole', _('Blackhole'));
+               o.optional = true;
+               o.placeholder = 'name';
+       },
+
+       renderStaticRoutes: function(m) {
+               var s, o;
+               var route;
+
+               route = uci.sections('keepalived', 'route');
+               if (route == '') {
+                       ui.addNotification(null, E('p', _('Routes must be configured for Static Routes')));
+               }
+
+               s = m.section(form.GridSection, 'static_routes', _('Static Routes'),
+                       _('Static Routes are not moved by vrrpd, they stay on the machine.') + '<br/>' +                             
+                       _('If you already have routes on your machines and your machines can ping each other, you don\'t need this section'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.DynamicList, 'route', _('Route'),
+                       _('List of Route Object'));
+               for (var i = 0; i < route.length; i++) {
+                       o.value(route[i]['name']);
+               }
+               o.optional = true;
+       },
+
+       render: function() {
+               var m;
+
+               m = new form.Map('keepalived');
+
+               this.renderRoute(m);
+               this.renderStaticRoutes(m);
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/script.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/script.js
new file mode 100644 (file)
index 0000000..99d5af2
--- /dev/null
@@ -0,0 +1,106 @@
+'use strict';
+'require view';
+'require ui';
+'require form';
+'require uci';
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       uci.load('keepalived'),
+               ]);
+       },
+
+       renderTrackScript: function(m) {
+               var s, o;
+               var vrrp_scripts;
+
+               vrrp_scripts = uci.sections('keepalived', 'vrrp_script');
+               if (vrrp_scripts == '') {
+                       ui.addNotification(null, E('p', _('VRRP Scripts must be configured for Track Scripts')));
+               }
+
+               s = m.section(form.GridSection, 'track_script', _('Track Script'),
+                       _('Tracking scripts would be referenced from VRRP instances'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.optional = false;
+               o.rmempty = false;
+
+               o = s.option(form.ListValue, 'value', _('VRRP Script'));
+               o.optional = false;
+               o.rmempty = false;
+               if (vrrp_scripts != '') {
+                       for (i = 0; i < vrrp_scripts.length; i++) {
+                               o.value(vrrp_scripts[i]['name']);
+                       }
+               }
+
+               o = s.option(form.Value, 'weight', _('Weight'));
+               o.optional = true;
+               o.datatype = 'and(integer, range(-253, 253))';
+
+               o = s.option(form.ListValue, 'direction', _('Direction'));
+               o.optional = true;
+               o.default = '';
+               o.value('reverse', _('Reverse'));
+               o.value('noreverse', _('No Reverse'));
+       },
+
+       renderVRRPScript: function(m) {
+               var s, o;
+
+               s = m.section(form.GridSection, 'vrrp_script', _('VRRP Script'),
+                       _('Adds a script to be executed periodically. Its exit code will be recorded for all VRRP instances and sync groups which are monitoring it'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.optional = true;
+               o.placeholder = 'name';
+
+               o = s.option(form.FileUpload, 'script', _('Script'),
+                       _('Path of the script to execute'));
+               o.root_directory = '/etc/keepalived/scripts';
+               o.enable_upload = true;
+               o.optional = true;
+               o.datatype = 'file';
+
+               o = s.option(form.Value, 'interval', _('Interval'),
+                       _('Seconds between script invocations'));
+               o.optional = true;
+               o.datatype = 'uinteger';
+               o.default = 60;
+
+               o = s.option(form.Value, 'weight', _('Weight'),
+                       _('Adjust script execution priority'));
+               o.optional = true;
+               o.datatype = 'and(integer, range(-253, 253))';
+
+               o = s.option(form.Value, 'rise', _('Rise'),
+                       _('Required number of successes for OK transition'));
+               o.optional = true;
+               o.datatype = 'uinteger';
+
+               o = s.option(form.Value, 'fail', _('Fail'),
+                       _('Required number of successes for KO transition'));
+               o.optional = true;
+               o.datatype = 'uinteger';
+       },
+
+       render: function() {
+               var m;
+
+               m = new form.Map('keepalived');
+
+               this.renderVRRPScript(m);
+               this.renderTrackScript(m);
+
+               return m.render();
+       }
+
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/servers.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/servers.js
new file mode 100644 (file)
index 0000000..1756f4b
--- /dev/null
@@ -0,0 +1,204 @@
+'use strict';
+'require view';
+'require form';
+'require uci';
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       uci.load('keepalived'),
+               ]);
+       },
+
+       renderVirtualServer: function(m) {
+               var s, o;
+               var real_servers;
+
+               s = m.section(form.GridSection, 'virtual_server', _('Virtual Server'),
+                       _('A virtual server is a service configured to listen on a specific virtual IP.') + '<br/>' +
+                       _('A VIP address migrates from one LVS router to the other during a failover,') +
+                       _('thus maintaining a presence at that IP address'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               s.tab('general', _('General'));
+               s.tab('advanced', _('Advanced'));
+
+               o = s.taboption('general', form.Flag, 'enabled', _('Enable'));
+               o.optional = true;
+               o.placeholder = 'name';
+
+               o = s.taboption('general', form.Value, 'ipaddr', _('Address'),
+                       _('Address of the Server'));
+               o.datatype = 'ipaddr';
+
+               o = s.taboption('general', form.ListValue, 'protocol', _('Protocol'));
+               o.value('TCP');
+               o.value('UDP');
+               o.default = 'TCP';
+               o.modalonly = true;
+
+               o = s.taboption('general', form.Value, 'port', _('Port'),
+                       _('Server Port'));
+               o.rmempty = false;
+               o.optional = false;
+               o.datatype = 'port';
+
+               o = s.taboption('general', form.Value, 'fwmark', _('Mark'),
+                       _('Firewall fwmark. Use Virtual server from FWMARK'));
+               o.datatype = 'hexstring';
+
+               real_servers = uci.sections('keepalived', 'real_server');
+               o = s.taboption('general', form.DynamicList, 'real_server', _('Real Server'));
+               if (real_servers != '') {
+                       for (i = 0; i < real_servers.length; i++) {
+                               o.value(real_servers[i]['name']);
+                       }
+               }
+               o.optional = false;
+
+               o = s.taboption('general', form.Value, 'virtualhost', _('Virtual Host'),
+                       _('HTTP virtualhost to use for HTTP_GET | SSL_GET'));
+               o.datatype = 'hostname';
+               o.modalonly = true;
+
+               o = s.taboption('general', form.ListValue, 'lb_kind', _('Forwarding Method'));
+               o.value('NAT');
+               o.value('DR');
+               o.value('TUN');
+               o.default = 'NAT';
+
+               o = s.taboption('advanced', form.Value, 'delay_loop', _('Delay Loop'),
+                       _('Interval between checks in seconds'));
+               o.optional = false;
+               o.datatype = 'uinteger';
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.ListValue, 'lb_algo', _('Scheduler Algorigthm'));
+               o.value('rr', _('Round-Robin'));
+               o.value('wrr', _('Weighted Round-Robin'));
+               o.value('lc', _('Least-Connection'));
+               o.value('wlc', _('Weighted Least-Connection'));
+               o.default = 'rr';
+
+               o = s.taboption('advanced', form.Value, 'persistence_timeout', _('Persist Timeout'),
+                       _('Timeout value for persistent connections'));
+               o.datatype = 'uinteger';
+               o.default = 50;
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'persistence_granularity', _('Persist Granularity'),
+                       _('Granularity mask for persistent connections'));
+               o.datatype = 'ipaddr';
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'sorry_server_ip', _('Sorry Server Address'),
+                       _('Server to be added to the pool if all real servers are down'));
+               o.optional = false;
+               o.datatype = 'ipaddr';
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'sorry_server_port', _('Sorry Server Port'));
+               o.optional = false;
+               o.datatype = 'port';
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'rise', _('Rise'),
+                       _('Required number of successes for OK transition'));
+               o.optional = true;
+               o.datatype = 'uinteger';
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'fail', _('Fail'),
+                       _('Required number of successes for KO transition'));
+               o.optional = true;
+               o.datatype = 'uinteger';
+               o.modalonly = true;
+       },
+
+       renderRealServer: function(m) {
+               var s, o;
+               var urls;
+
+               s = m.section(form.GridSection, 'real_server', _('Real Servers'),
+                       _('Real Server to redirect all request'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.rmempty = false;
+               o.optional = false;
+               o.placeholder = 'name';
+
+               o = s.option(form.Flag, 'enabled', _('Enabled'));
+               o.default = true;
+
+               o = s.option(form.Value, 'ipaddr', _('Address'),
+                       _('Address of the Server'));
+               o.rmempty = false;
+               o.optional = false;
+               o.datatype = 'ipaddr';
+
+               o = s.option(form.Value, 'port', _('Port'),
+                       _('Server Port'));
+               o.rmempty = false;
+               o.optional = false;
+               o.datatype = 'port';
+
+               o = s.option(form.Value, 'weight', _('Weight'),
+                       _('Relative weight to use'));
+               o.rmempty = false;
+               o.optional = false;
+               o.placeholder = 1;
+               o.datatype = 'uinteger';
+
+               o = s.option(form.ListValue, 'check', _('Check'),
+                       _('Healthcheckers. Can be multiple of each type'));
+               o.value('HTTP_GET');
+               o.value('SSL_GET');
+               o.value('TCP_CHECK');
+               o.value('MISC_CHECK');
+
+               o = s.option(form.Value, 'connect_timeout', _('Connect Timeout'));
+               o.datatype = 'uinteger';
+               o.depends('check', 'TCP_CHECK'); 
+
+               o = s.option(form.Value, 'connect_port', _('Port'),
+                       _('Port to connect to'));
+               o.datatype = 'port';
+               o.depends('check', 'TCP_CHECK'); 
+
+               o = s.option(form.Value, 'misc_path', _('User Check Script'));
+               o.datatype = 'file';
+               o.depends('check', 'MISC_CHECK'); 
+
+               urls = uci.sections('keepalived', 'url');
+               o = s.option(form.DynamicList, 'url', _('URLs'));
+               if (urls != '') {
+                       for (var i = 0; i < urls.length; i++) {
+                               o.value(urls[i].name);
+                       }
+               }
+               o.depends('check', 'HTTP_GET'); 
+               o.depends('check', 'SSL_GET'); 
+
+               o = s.option(form.Value, 'retry', _('Retry'));
+               o.datatype = 'uinteger';
+
+               o = s.option(form.Value, 'delay_before_retry', _('Delay Before Retry'));
+               o.datatype = 'uinteger';
+       },
+
+       render: function() {
+               var m;
+
+               m = new form.Map('keepalived');
+
+               this.renderVirtualServer(m);
+               this.renderRealServer(m);
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/track_interface.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/track_interface.js
new file mode 100644 (file)
index 0000000..b407d0e
--- /dev/null
@@ -0,0 +1,36 @@
+'use strict';
+'require view';
+'require form';
+'require tools.widgets as widgets';
+'require uci';
+
+return view.extend({
+       render: function() {
+               var m, s, o;
+
+               m = new form.Map('keepalived');
+
+               s = m.section(form.GridSection, 'track_interface', _('Track Interface'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.rmempty = false;
+               o.optional = false;
+
+               o = s.option(widgets.DeviceSelect, 'value', _('Device'),
+                       _('Device to track'));
+               o.noaliases = true;
+               o.rmempty = false;
+               o.optional = false;
+
+               o = s.option(form.Value, 'weight', _('Weight'),
+                       _('When a weight is specified, instead of setting the vrrp_instance to the FAULT state in case of failure, ') +
+                       _('its priority will be increased or decreased by the weight when the interface is up or down'));
+               o.optional = false;
+               o.datatype = 'uinteger';
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/url.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/url.js
new file mode 100644 (file)
index 0000000..5e311fd
--- /dev/null
@@ -0,0 +1,30 @@
+'use strict';
+'require view';
+'require form';
+
+return view.extend({
+       render: function() {
+               var m, s, o;
+
+               m = new form.Map('keepalived');
+
+               s = m.section(form.GridSection, 'url', _('URLs'),
+                       _('URLs can be referenced into Real Servers to test'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.optional = false;
+
+               o = s.option(form.Value, 'path', _('URL Path'),
+                       _('URL path, i.e path /, or path /mrtg2/'));
+               o.optional = false;
+
+               o = s.option(form.Value, 'digest', _('Digest'),
+                       _('Digest computed with genhash'));
+               o.datatype = 'length(32)';
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_instance.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_instance.js
new file mode 100644 (file)
index 0000000..f9293d6
--- /dev/null
@@ -0,0 +1,310 @@
+'use strict';
+'require view';
+'require form';
+'require uci';
+'require network';
+'require tools.widgets as widgets';
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       network.getDevices(),
+                       uci.load('keepalived'),
+               ]);
+       },
+
+       renderGeneralTab: function(s) {
+               var o, ipaddress;
+
+               o = s.taboption('general',form.Value, 'name', _('Name'));
+               o.rmempty = false;
+               o.optional = false;
+
+               o = s.taboption('general', form.ListValue, 'state', _('State'),
+                       _('Initial State. As soon as the other machine(s) come up,') +
+                       _('an election will be held and the machine with the highest "priority" will become MASTER.'));
+               o.value('MASTER', _('Master'));
+               o.value('BACKUP', _('Backup'));
+               o.optional = false;
+               o.rmempty = false;
+
+               o = s.taboption('general', widgets.DeviceSelect, 'interface', _('Interface'),
+                       _('Interface for inside_network, bound by VRRP'));
+               o.noaliases = true;
+               o.noinactive = true;
+               o.optional = false;
+               o.rmempty = false;
+
+               o = s.taboption('general', form.Value, 'virtual_router_id', _('Virtual Router Id'),
+                       _('Differentiate multiple instances of vrrpd, running on the same NIC'));
+               o.datatype = 'range(1-255)';
+               o.optional = false;
+               o.rmempty = false;
+
+               o = s.taboption('general', form.Value, 'priority', _('Priority'),
+                       _('A server with a higher priority becomes a MASTER'));
+               o.datatype = 'uinteger';
+               o.optional = false;
+               o.rmempty = false;
+
+               o = s.taboption('general', form.ListValue, 'advert_int', _('Interval'),
+                       _('VRRP Advert interval in seconds'));
+               o.datatype = 'float';
+               o.default = '1';
+               o.rmempty = false;
+               o.optional = false;
+               o.value('1');
+               o.value('3');
+               o.value('5');
+               o.value('10');
+               o.value('30');
+               o.value('60');
+
+               o = s.taboption('general', form.Flag, 'nopreempt', _('Disable Preempt'),
+                       _('Allows the lower priority machine to maintain the master role,') +
+                       _('even when a higher priority machine comes back online.') + ' ' +
+                       _('For this to work, the initial state of this entry must be BACKUP.'));
+               o.default = false;
+               o.rmempty = false;
+
+               ipaddress = uci.sections('keepalived', 'ipaddress');
+               o = s.taboption('general', form.DynamicList, 'virtual_ipaddress', _('Virtual IP Address'),
+                       _('Addresses add|del on change to MASTER, to BACKUP.') + ' ' +
+                       _('With the same entries on other machines, the opposite transition will be occurring.'));
+               if (ipaddress != '') {
+                       for (var i = 0; i < ipaddress.length; i++) {
+                               o.value(ipaddress[i]['name']);
+                       }
+               }
+               o.rmempty = false;
+               o.optional = false;
+       },
+
+       renderPeerTab: function(s, netDevs) {
+               var o;
+
+               o = s.taboption('peer', form.ListValue, 'unicast_src_ip', _('Unicast Source IP'),
+                       _('Default IP for binding vrrpd is the primary IP on interface'));
+               o.datatype = 'ipaddr';
+               o.optional = true;
+               o.modalonly = true;
+               for (var i = 0; i < netDevs.length; i++) {
+                       var addrs = netDevs[i].getIPAddrs();
+                       for (var j = 0; j < addrs.length; j++) {
+                               o.value(addrs[j].split('/')[0]);
+                       }
+               }
+
+               var peers = uci.sections('keepalived', 'peer');
+               o = s.taboption('peer', form.DynamicList, 'unicast_peer', _('Peer'),
+                       _('Do not send VRRP adverts over VRRP multicast group.') + ' ' +
+                       _('Instead it sends adverts to the following list of ip addresses using unicast design fashion'));
+               if (peers != '') {
+                       for (var i = 0; i < peers.length; i++) {
+                               o.value(peers[i]['name']);
+                       }
+               }
+
+               o = s.taboption('peer', form.Value, 'mcast_src_ip', _('Multicast Source IP'),
+                       _('If you want to hide location of vrrpd, use this IP for multicast vrrp packets'));
+               o.datatype = 'ipaddr';
+               o.optional = true;
+               o.modalonly = true;
+               o.depends({ 'unicast_peer' : '' });
+
+               o = s.taboption('peer', form.ListValue, 'auth_type', _('HA Authentication Type'));
+               o.value('PASS', _('Simple Password'));
+               o.value('AH', _('IPSec'));
+
+               o = s.taboption('peer', form.Value, 'auth_pass', _('Password'),
+                       _('Password for accessing vrrpd, should be the same on all machines'));
+               o.datatype = 'maxlength(8)';
+               o.password = true;
+               o.modalonly = true;
+               o.depends({ 'auth_type' : 'PASS' });
+       },
+
+       renderGARPTab: function(s) {
+               var o;
+
+               o = s.taboption('garp', form.ListValue, 'garp_master_delay', _('GARP Delay'),
+                       _('Gratuitous Master Delay in seconds'));
+               o.datatype = 'uinteger';
+               o.modalonly = true;
+               o.value('1');
+               o.value('3');
+               o.value('5');
+               o.value('10');
+               o.value('30');
+               o.value('60');
+
+               o = s.taboption('garp', form.ListValue, 'garp_master_repeat', _('GARP Repeat'),
+                       _('Gratuitous Master Repeat in seconds'));
+               o.datatype = 'uinteger';
+               o.modalonly = true;
+               o.value('1');
+               o.value('3');
+               o.value('5');
+               o.value('10');
+               o.value('30');
+               o.value('60');
+
+               o = s.taboption('garp', form.ListValue, 'garp_master_refresh', _('GARP Refresh'),
+                       _('Gratuitous Master Refresh in seconds'));
+               o.datatype = 'uinteger';
+               o.modalonly = true;
+               o.value('1');
+               o.value('3');
+               o.value('5');
+               o.value('10');
+               o.value('30');
+               o.value('60');
+
+               o = s.taboption('garp', form.ListValue, 'garp_master_refresh_repeat', _('GARP Refresh Repeat'),
+                       _('Gratuitous Master Refresh Repeat in seconds'));
+               o.datatype = 'uinteger';
+               o.modalonly = true;
+               o.value('1');
+               o.value('3');
+               o.value('5');
+               o.value('10');
+               o.value('30');
+               o.value('60');
+       },
+
+       renderAdvancedTab: function(s) {
+               var o;
+
+               o = s.taboption('advanced', form.Value, 'use_vmac', _('Use VMAC'),
+                       _('Use VRRP Virtual MAC'));
+               o.optional = true;
+               o.placeholder = '[<VMAC_INTERFACE_NAME>] [MAC_ADDRESS]';
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Flag, 'vmac_xmit_base', _('Use VMAC Base'),
+                       _('Send/Recv VRRP messages from base interface instead of VMAC interfac'));
+               o.default = false;
+               o.optional = true;
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Flag, 'native_ipv6', _('Use IPV6'),
+                       _('Force instance to use IPv6'));
+               o.default = false;
+               o.optional = true;
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Flag, 'dont_track_primary', _('Disable Primary Tracking'),
+                       _('Ignore VRRP interface faults'));
+               o.default = false;
+               o.optional = true;
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.ListValue, 'version', _('Version'),
+                       _('VRRP version to run on interface'));
+               o.value('', _('None'));
+               o.value('2', _('2'));
+               o.value('3', _('3'));
+               o.default = '';
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Flag, 'accept', _('Accept'),
+                       _('Accept packets to non address-owner'));
+               o.default = false;
+               o.optional = true;
+
+               o = s.taboption('advanced', form.Value, 'preempt_delay', _('Preempt Delay'),
+                       _('Time in seconds to delay preempting compared'));
+               o.datatype = 'float';
+               o.placeholder = '300';
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.ListValue, 'preempt_delay', _('Debug'),
+                       _('Debug Level'));
+               o.default = '0';
+               o.value('0');
+               o.value('1');
+               o.value('2');
+               o.value('3');
+               o.value('4');
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Flag, 'smtp_alert', _('Email Alert'),
+                       _('Send SMTP alerts'));
+               o.default = false;
+               o.modalonly = true;
+       },
+
+       renderTrackingTab: function(s) {
+               var o;
+               var ipaddress, routes, interfaces, scripts;
+
+               ipaddress = uci.sections('keepalived', 'ipaddress');
+               routes = uci.sections('keepalived', 'route');
+               interfaces = uci.sections('keepalived', 'track_interface');
+               scripts = uci.sections('keepalived', 'track_script');
+
+               o = s.taboption('tracking', form.DynamicList, 'virtual_ipaddress_excluded', _('Exclude Virtual IP Address'),
+                       _('VRRP IP excluded from VRRP. For cases with large numbers (eg 200) of IPs on the same interface.') + ' ' +
+                       _('To decrease the number of packets sent in adverts, you can exclude most IPs from adverts.'));
+               o.modalonly = true;
+               if (ipaddress != '') {
+                       for (var i = 0; i < ipaddress.length; i++) {
+                               o.value(ipaddress[i]['name']);
+                       }
+               }
+
+               o = s.taboption('tracking', form.DynamicList, 'virtual_routes', _('Virtual Routes'),
+                       _('Routes add|del when changing to MASTER, to BACKUP'));
+               o.modalonly = true;
+               if (routes != '') {
+                       for (var i = 0; i < routes.length; i++) {
+                               o.value(routes[i]['name']);
+                       }
+               }
+
+               o = s.taboption('tracking', form.DynamicList, 'track_interface', _('Track Interfaces'),
+                       _('Go to FAULT state if any of these go down'));
+               o.modalonly = true;
+               if (interfaces != '') {
+                       for (var i = 0; i < interfaces.length; i++) {
+                               o.value(interfaces[i]['name']);
+                       }
+               }
+
+               o = s.taboption('tracking', form.DynamicList, 'track_script', _('Track Script'),
+                       _('Go to FAULT state if any of these go down, if unweighted'));
+               o.modalonly = true;
+               if (scripts != '') {
+                       for (var i = 0; i < scripts.length; i++) {
+                               o.value(scripts[i]['name']);
+                       }
+               }
+       },
+
+       render: function(data) {
+               var netDevs = data[0];
+               var m, s, o;
+
+               m = new form.Map('keepalived');
+
+               s = m.section(form.GridSection, 'vrrp_instance', _('VRRP Instance'),
+                       _('Define an individual instance of the VRRP protocol running on an interface'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.tab('general', _('General'));
+               o = s.tab('peer', _('Peer'));
+               o = s.tab('tracking', _('Tracking'));
+               o = s.tab('garp', _('GARP'));
+               o = s.tab('advanced', _('Advanced'));
+
+               this.renderGeneralTab(s);
+               this.renderPeerTab(s, netDevs);
+               this.renderTrackingTab(s);
+               this.renderGARPTab(s);
+               this.renderAdvancedTab(s);
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_sync_group.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_sync_group.js
new file mode 100644 (file)
index 0000000..69ed8f2
--- /dev/null
@@ -0,0 +1,57 @@
+'use strict';
+'require view';
+'require ui';
+'require form';
+'require uci';
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       uci.load('keepalived'),
+               ]);
+       },
+
+       render: function(data) {
+               var m, s, o;
+               var instances;
+
+               instances = uci.sections('keepalived', 'vrrp_instance');
+               if (instances == '' || instances.length < 1) {
+                       ui.addNotification(null, E('p', _('Instances must be configured for VRRP Groups')));
+               }
+
+               m = new form.Map('keepalived');
+
+               s = m.section(form.GridSection, 'vrrp_sync_group', _('VRRP synchronization group'),
+                       _('VRRP Sync Group is an extension to VRRP protocol.') + '<br/>' +
+                       _('The main goal is to define a bundle of VRRP instance to get synchronized together') + '<br/>' +
+                       _('so that transition of one instance will be reflected to others group members'));
+               s.anonymous = true;
+               s.addremove = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.Value, 'name', _('Name'));
+               o.rmempty = false;
+               o.optional = false;
+               o.placeholder = 'name';
+
+               o = s.option(form.DynamicList, 'group', _('Instance Group'));
+               o.rmempty = false;
+               o.optional = false;
+               for (var i = 0; i < instances.length; i++) {
+                       o.value(instances[i]['name']);
+               }
+
+               o = s.option(form.Flag, 'smtp_alert', _('Email Notification'),
+                       _('Send email notification during state transition'));
+               o.optional = true;
+               o.default = false;
+
+               o = s.option(form.Flag, 'global_tracking', _('Global Tracking'),
+                       _('Track interfaces, scripts and files'));
+               o.optional = true;
+               o.default = false;
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-keepalived/htdocs/luci-static/resources/view/status/include/35_keepalived.js b/applications/luci-app-keepalived/htdocs/luci-static/resources/view/status/include/35_keepalived.js
new file mode 100644 (file)
index 0000000..4f47d14
--- /dev/null
@@ -0,0 +1,65 @@
+'use strict';
+'require baseclass';
+'require uci';
+'require rpc';
+
+var callKeepalivedStatus = rpc.declare({
+       object: 'keepalived',
+       method: 'dump',
+       expect: {  },
+});
+
+return baseclass.extend({
+       title: _('Keepalived Instances'),
+
+       load: function() {
+               return Promise.all([
+                       callKeepalivedStatus(),
+                       uci.load('keepalived'),
+               ]);
+       },
+
+       render: function(data) {
+               var targets = (data[0].status) ? data[0].status : [];
+               var instances = uci.sections('keepalived', 'vrrp_instance');
+
+               var table =
+                       E('table', { 'class': 'table lases' }, [
+                               E('tr', { 'class': 'tr table-titles' }, [
+                                       E('th', { 'class': 'th' }, _('Name')),
+                                       E('th', { 'class': 'th' }, _('Interface')),
+                                       E('th', { 'class': 'th' }, _('Active State/State')),
+                                       E('th', { 'class': 'th' }, _('Probes Sent')),
+                                       E('th', { 'class': 'th' }, _('Probes Received')),
+                                       E('th', { 'class': 'th' }, _('Last Transition')),
+                                       E([])
+                               ])
+                       ]);
+
+               cbi_update_table(table,
+                       targets.map(function(target) {
+                               var state = (target.stats.become_master - target.stats.release_master) ? 'MASTER' : 'BACKUP';
+                               if (instances != '') {
+                                       for (var i = 0; i < instances.length; i++) {
+                                               if (instances[i]['name'] == target.data.iname) {
+                                                       state = state + '/' + instances[i]['state'];
+                                                       break;
+                                               }
+                                       }
+                               }
+                               return  [ 
+                                       target.data.iname,
+                                       target.data.ifp_ifname,
+                                       state,
+                                       target.stats.advert_sent,
+                                       target.stats.advert_rcvd,
+                                       new Date(target.data.last_transition * 1000)
+                               ];      
+                       }, this), E('em', _('There are no active instances')));
+               
+
+               return E([
+                       table
+               ]);
+       },
+});
diff --git a/applications/luci-app-keepalived/root/usr/share/luci/menu.d/luci-app-keepalived.json b/applications/luci-app-keepalived/root/usr/share/luci/menu.d/luci-app-keepalived.json
new file mode 100644 (file)
index 0000000..d839ab9
--- /dev/null
@@ -0,0 +1,109 @@
+{
+       "admin/services/keepalived": {
+               "title": "Keepalived",
+               "order": 1,
+               "action": {
+                       "type": "alias",
+                       "path": "admin/services/keepalived/overview"
+               }
+       },
+
+       "admin/services/keepalived/overview": {
+               "title": "Overview",
+               "order": 10,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/overview"
+               }
+       },
+
+       "admin/services/keepalived/globals": {
+               "title": "Globals",
+               "order": 20,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/globals"
+               }
+       },
+
+       "admin/services/keepalived/ipaddress": {
+               "title": "IP Address",
+               "order": 30,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/ipaddress"
+               }
+       },
+
+       "admin/services/keepalived/route": {
+               "title": "Route",
+               "order": 40,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/route"
+               }
+       },
+
+       "admin/services/keepalived/url": {
+               "title": "URLs",
+               "order": 50,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/url"
+               }
+       },
+
+       "admin/services/keepalived/script": {
+               "title": "Scripts",
+               "order": 80,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/script"
+               }
+       },
+
+       "admin/services/keepalived/track_interface": {
+               "title": "Interfaces",
+               "order": 90,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/track_interface"
+               }
+       },
+
+       "admin/services/keepalived/peers": {
+               "title": "Peers",
+               "order": 110,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/peers"
+               }
+       },
+
+       "admin/services/keepalived/vrrp_instance": {
+               "title": "Instance",
+               "order": 110,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/vrrp_instance"
+               }
+       },
+
+       "admin/services/keepalived/servers": {
+               "title": "Servers",
+               "order": 120,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/servers"
+               }
+       },
+
+       "admin/services/keepalived/vrrp_sync_group": {
+               "title": "Sync Group",
+               "order": 140,
+               "action": {
+                       "type": "view",
+                       "path": "keepalived/vrrp_sync_group"
+               }
+       }
+}
diff --git a/applications/luci-app-keepalived/root/usr/share/rpcd/acl.d/luci-app-keepalived.json b/applications/luci-app-keepalived/root/usr/share/rpcd/acl.d/luci-app-keepalived.json
new file mode 100644 (file)
index 0000000..0c8b676
--- /dev/null
@@ -0,0 +1,17 @@
+{
+       "luci-app-keepalived" : {
+               "description" : "Grant access to LuCI app keepalived",
+               "read" : {
+                       "ubus" : {
+                               "keepalived" : [ "*" ]
+                       },
+                       "uci": [ "keepalived" ]
+               },
+               "write" : {
+                       "uci": [ "keepalived" ],
+                       "file" : {
+                               "/etc/keepalived/keys/*" : [ "write" ]
+                       }
+               }
+       }
+}