Browse Source

luci-app-keepalived: Add LuCI for keepalived

LuCI Support for Keepalived

Signed-off-by: Jaymin Patel <jem.patel@gmail.com>
Jaymin Patel 1 year ago
parent
commit
d1a82d2886
15 changed files with 1376 additions and 0 deletions
  1. 18 0
      applications/luci-app-keepalived/Makefile
  2. 66 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/globals.js
  3. 90 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/ipaddress.js
  4. 75 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/overview.js
  5. 97 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/peers.js
  6. 96 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/route.js
  7. 106 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/script.js
  8. 204 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/servers.js
  9. 36 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/track_interface.js
  10. 30 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/url.js
  11. 310 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_instance.js
  12. 57 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_sync_group.js
  13. 65 0
      applications/luci-app-keepalived/htdocs/luci-static/resources/view/status/include/35_keepalived.js
  14. 109 0
      applications/luci-app-keepalived/root/usr/share/luci/menu.d/luci-app-keepalived.json
  15. 17 0
      applications/luci-app-keepalived/root/usr/share/rpcd/acl.d/luci-app-keepalived.json

+ 18 - 0
applications/luci-app-keepalived/Makefile

@@ -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
+

+ 66 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/globals.js

@@ -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();
+	}
+});

+ 90 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/ipaddress.js

@@ -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();
+	}
+});

+ 75 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/overview.js

@@ -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
+});

+ 97 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/peers.js

@@ -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();
+	}
+});

+ 96 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/route.js

@@ -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();
+	}
+});

+ 106 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/script.js

@@ -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();
+	}
+
+});

+ 204 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/servers.js

@@ -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();
+	}
+});

+ 36 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/track_interface.js

@@ -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();
+	}
+});

+ 30 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/url.js

@@ -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();
+	}
+});

+ 310 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_instance.js

@@ -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();
+	}
+});

+ 57 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_sync_group.js

@@ -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();
+	}
+});

+ 65 - 0
applications/luci-app-keepalived/htdocs/luci-static/resources/view/status/include/35_keepalived.js

@@ -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
+		]);
+	},
+});

+ 109 - 0
applications/luci-app-keepalived/root/usr/share/luci/menu.d/luci-app-keepalived.json

@@ -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"
+		}
+	}
+}

+ 17 - 0
applications/luci-app-keepalived/root/usr/share/rpcd/acl.d/luci-app-keepalived.json

@@ -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" ]
+			}
+		}
+	}
+}