From d1a82d28868678716f16472f70b46557ba99f8df Mon Sep 17 00:00:00 2001 From: Jaymin Patel Date: Thu, 8 Sep 2022 10:05:13 +0530 Subject: [PATCH] luci-app-keepalived: Add LuCI for keepalived LuCI Support for Keepalived Signed-off-by: Jaymin Patel --- applications/luci-app-keepalived/Makefile | 18 + .../resources/view/keepalived/globals.js | 66 ++++ .../resources/view/keepalived/ipaddress.js | 90 +++++ .../resources/view/keepalived/overview.js | 75 +++++ .../resources/view/keepalived/peers.js | 97 ++++++ .../resources/view/keepalived/route.js | 96 ++++++ .../resources/view/keepalived/script.js | 106 ++++++ .../resources/view/keepalived/servers.js | 204 ++++++++++++ .../view/keepalived/track_interface.js | 36 ++ .../resources/view/keepalived/url.js | 30 ++ .../view/keepalived/vrrp_instance.js | 310 ++++++++++++++++++ .../view/keepalived/vrrp_sync_group.js | 57 ++++ .../view/status/include/35_keepalived.js | 65 ++++ .../luci/menu.d/luci-app-keepalived.json | 109 ++++++ .../share/rpcd/acl.d/luci-app-keepalived.json | 17 + 15 files changed, 1376 insertions(+) create mode 100644 applications/luci-app-keepalived/Makefile create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/globals.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/ipaddress.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/overview.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/peers.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/route.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/script.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/servers.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/track_interface.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/url.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_instance.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/keepalived/vrrp_sync_group.js create mode 100644 applications/luci-app-keepalived/htdocs/luci-static/resources/view/status/include/35_keepalived.js create mode 100644 applications/luci-app-keepalived/root/usr/share/luci/menu.d/luci-app-keepalived.json create mode 100644 applications/luci-app-keepalived/root/usr/share/rpcd/acl.d/luci-app-keepalived.json diff --git a/applications/luci-app-keepalived/Makefile b/applications/luci-app-keepalived/Makefile new file mode 100644 index 0000000000..81b0cc2635 --- /dev/null +++ b/applications/luci-app-keepalived/Makefile @@ -0,0 +1,18 @@ +# +# Copyright (C) 2022 Jaymin Patel +# +# 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 + +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 index 0000000000..5329d3304c --- /dev/null +++ b/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 []'; + + 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 index 0000000000..0cdce65bef --- /dev/null +++ b/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.') + '
' + + _('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 index 0000000000..7e261bf82d --- /dev/null +++ b/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 +}); 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 index 0000000000..059fc1dd6c --- /dev/null +++ b/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.') + '
' + + _('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 index 0000000000..cf2454c7d4 --- /dev/null +++ b/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.') + '
' + + _('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 index 0000000000..99d5af29f2 --- /dev/null +++ b/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(); + } + +}); 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 index 0000000000..1756f4b9fa --- /dev/null +++ b/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.') + '
' + + _('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 index 0000000000..b407d0eef8 --- /dev/null +++ b/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(); + } +}); 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 index 0000000000..5e311fd255 --- /dev/null +++ b/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(); + } +}); 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 index 0000000000..f9293d6c20 --- /dev/null +++ b/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 = '[] [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 index 0000000000..69ed8f2435 --- /dev/null +++ b/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.') + '
' + + _('The main goal is to define a bundle of VRRP instance to get synchronized together') + '
' + + _('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 index 0000000000..4f47d14980 --- /dev/null +++ b/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 + ]); + }, +}); 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 index 0000000000..d839ab935c --- /dev/null +++ b/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" + } + } +} 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 index 0000000000..0c8b676e61 --- /dev/null +++ b/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" ] + } + } + } +} -- 2.30.2