Implement NodeBalancers

This commit is contained in:
2024-08-08 19:46:58 -04:00
parent 35e9d259f5
commit 709a220e1b
22 changed files with 2585 additions and 23 deletions

View File

@ -0,0 +1,43 @@
/*
* This file is part of Linode Manager Classic.
*
* Linode Manager Classic is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Linode Manager Classic is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
@import url('/global.css');
.center-cell {
text-align: center;
}
.check-show {
display: none;
}
#config {
padding: 0px 15px 15px;
}
#config-table tbody:not(.lmc-tbody-head) tr td:first-of-type {
font-weight: bold;
text-align: right;
}
#config-table tbody:not(.lmc-tbody-head) tr td:not(:last-of-type) {
white-space: nowrap;
}
.protocol-show {
display: none;
}

View File

@ -0,0 +1,394 @@
/*
* This file is part of Linode Manager Classic.
*
* Linode Manager Classic is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Linode Manager Classic is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
*/
import { settings, elements, apiDelete, apiGet, apiPost, apiPut, parseParams, setupHeader } from "/global.js";
(function()
{
// Element names specific to this page
elements.algorithm = "algorithm";
elements.centerCell = "center-cell";
elements.checkAttempts = "check-attempts";
elements.checkBody = "check-body";
elements.checkInterval = "check-interval";
elements.checkPath = "check-path";
elements.checkShow = "check-show";
elements.checkTimeout = "check-timeout";
elements.checkType = "check-type";
elements.cipherSuite = "cipher-suite";
elements.commonName = "common-name";
elements.configLabel = "config-label";
elements.fingerprint = "fingerprint";
elements.lmcRow = "lmc-tr1";
elements.lmcRowAlt = "lmc-tr2";
elements.nodebalancerLabel = "nodebalancer-label";
elements.nodebalancerTag = "nodebalancer-tag";
elements.nodebalancerTagLink = "nodebalancer-tag-link";
elements.nodesTable = "nodes-table";
elements.nonzero = "nonzero";
elements.passive = "passive";
elements.port = "port";
elements.protocol = "protocol";
elements.protocolShow = "protocol-show";
elements.proxyProtocol = "proxy-protocol";
elements.removePrefix = "remove-node-";
elements.replaceCert = "replace-cert";
elements.replaceCertOnly = "replace-cert-only";
elements.replaceLink = "replace-link";
elements.saveButton = "save-button";
elements.sslCert = "ssl-cert";
elements.sslKey = "ssl-key";
elements.stickiness = "stickiness";
// Data recieved from API calls
var data = {};
data.config = {};
data.nodebalancer = {};
data.nodes = [];
// Static references to UI elements
var ui = {};
ui.algorithm = {};
ui.checkAttempts = {};
ui.checkBody = {};
ui.checkInterval = {};
ui.checkPath = {};
ui.checkShow = [];
ui.checkTimeout = {};
ui.checkType = {};
ui.cipherSuite = {};
ui.commonName = {};
ui.configLabel = {};
ui.fingerprint = {};
ui.nodebalancerLabel = {};
ui.nodebalancerTag = {};
ui.nodebalancerTagLink = {};
ui.nodesTable = {};
ui.passive = {};
ui.port = {};
ui.protocol = {};
ui.protocolShow = [];
ui.proxyProtocol = {};
ui.saveButton = {};
ui.replaceLink = {};
ui.sslCert = {};
ui.sslKey = {};
ui.stickiness = {};
// Temporary State
var state = {};
state.replaceCert = true;
// Create a row for the nodes table
var createNodeRow = function(node, alt)
{
var row = document.createElement("tr");
if (alt)
row.className = elements.lmcRowAlt;
else
row.className = elements.lmcRow;
var label = document.createElement("td");
label.innerHTML = node.label;
row.appendChild(label);
var address = document.createElement("td");
var lastColon = node.address.lastIndexOf(":");
address.innerHTML = node.address.slice(0, lastColon);
row.appendChild(address);
var port = document.createElement("td");
port.innerHTML = node.address.slice(lastColon + 1);
row.appendChild(port);
var weight = document.createElement("td");
weight.innerHTML = node.weight;
row.appendChild(weight);
var mode = document.createElement("td");
mode.innerHTML = node.mode.charAt(0).toUpperCase() + node.mode.slice(1);
row.appendChild(mode);
var status = document.createElement("td");
status.innerHTML = node.status;
row.appendChild(status);
var options = document.createElement("td");
options.className = elements.centerCell;
var editLink = document.createElement("a");
editLink.href = "/nodebalancers/node?nbid=" + data.params.nbid + "&nbcid=" + data.params.nbcid + "&nbnid=" + node.id;
editLink.innerHTML = "Edit";
var separator = document.createElement("span");
separator.innerHTML = " | ";
var removeLink = document.createElement("a");
removeLink.id = elements.removePrefix + node.id;
removeLink.href = "#";
removeLink.innerHTML = "Remove";
removeLink.addEventListener("click", handleRemoveNode);
options.appendChild(editLink);
options.appendChild(separator);
options.appendChild(removeLink);
row.appendChild(options);
return row;
};
// Callback for config API call
var displayConfig = function(response)
{
data.config = response;
ui.configLabel.innerHTML = "Port " + data.config.port;
ui.port.value = data.config.port;
ui.protocol.value = data.config.protocol;
ui.proxyProtocol.value = data.config.proxy_protocol;
ui.algorithm.value = data.config.algorithm;
ui.stickiness.value = data.config.stickiness;
ui.commonName.innerHTML = data.config.ssl_commonname;
ui.fingerprint.innerHTML = data.config.ssl_fingerprint;
ui.cipherSuite.value = data.config.cipher_suite;
ui.checkType.value = data.config.check;
ui.checkInterval.value = data.config.check_interval;
ui.checkTimeout.value = data.config.check_timeout;
ui.checkAttempts.value = data.config.check_attempts;
ui.checkPath.value = data.config.check_path;
ui.checkBody.value = data.config.check_body;
ui.passive.checked = data.config.check_passive;
state.replaceCert = (!data.config.ssl_key || data.config.ssl_key.indexOf("<REDACTED>") == -1);
ui.saveButton.disabled = false;
showHideChecks();
showHideProtocol();
};
// Callback for nodebalancer API call
var displayNodebalancer = function(response)
{
data.nodebalancer = response;
// Set page title and header stuff
if (document.title.indexOf("//") == -1)
document.title += " // Edit " + data.nodebalancer.label;
ui.nodebalancerLabel.innerHTML = data.nodebalancer.label;
if (data.nodebalancer.tags.length == 1) {
ui.nodebalancerTagLink.href = "/nodebalancers?tag=" + data.nodebalancer.tags[0];
ui.nodebalancerTagLink.innerHTML = "(" + data.nodebalancer.tags[0] + ")";
ui.nodebalancerTag.style.display = "inline";
} else {
ui.nodebalancerTag.style.display = "none";
}
};
// Callback for nodes API call
var displayNodes = function(response)
{
data.nodes = data.nodes.concat(response.data);
// Request the next page if there are more pages
if (response.page != response.pages) {
apiGet("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes?page=" + (response.page + 1), displayNodes, null);
return;
}
// Insert nodes into table
for (var i = 0; i < data.nodes.length; i++)
ui.nodesTable.appendChild(createNodeRow(data.nodes[i], i % 2));
};
// Remove node handler
var handleRemoveNode = function(event)
{
if (!confirm("Are you sure you want to remove this node?"))
return;
var nbnid = event.currentTarget.id.substring(elements.removePrefix.length);
apiDelete("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes/" + nbnid, function()
{
location.reload();
});
};
// Replace link handler
var handleReplace = function(event)
{
state.replaceCert = true;
showHideProtocol();
};
// Save button handler
var handleSave = function(event)
{
if (event.currentTarget.disabled)
return;
var req = {
"port": parseInt(ui.port.value),
"protocol": ui.protocol.value,
"algorithm": ui.algorithm.value,
"stickiness": ui.stickiness.value,
"check": ui.checkType.value,
"check_passive": ui.passive.checked
};
if (ui.protocol.value == "tcp") {
req.proxy_protocol = ui.proxyProtocol.value;
} else if (ui.protocol.value == "https") {
req.cipher_suite = ui.cipherSuite.value;
if (state.replaceCert) {
req.ssl_cert = ui.sslCert.value;
req.ssl_key = ui.sslKey.value;
}
}
switch (ui.checkType.value) {
case "http_body":
req.check_body = ui.checkBody.value;
case "http":
req.check_path = ui.checkPath.value;
case "connection":
req.check_interval = parseInt(ui.checkInterval.value);
req.check_timeout = parseInt(ui.checkTimeout.value);
req.check_attempts = parseInt(ui.checkAttempts.value);
}
if (data.params.nbcid == 0) {
apiPost("/nodebalancers/" + data.params.nbid + "/configs", req, function(response)
{
location.href = "/nodebalancers/config?nbid=" + data.params.nbid + "&nbcid=" + response.id;
});
} else {
apiPut("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid, req, function(response)
{
location.href = "/nodebalancers/balancer?nbid=" + data.params.nbid;
});
}
};
// Initial setup
var setup = function()
{
// Parse URL parameters
data.params = parseParams();
// We need a NodeBalancer ID, so die if we don't have it
if (!data.params.nbid) {
alert("No NodeBalancer ID supplied!");
return;
}
// We also need a config ID
if (!data.params.nbcid) {
alert("No config ID supplied!");
return;
}
setupHeader();
// Update links on page to include proper Linode ID
var anchors = document.getElementsByTagName("a");
for (var i = 0; i < anchors.length; i++) {
anchors[i].href = anchors[i].href.replace("nbid=0", "nbid=" + data.params.nbid);
anchors[i].href = anchors[i].href.replace("nbcid=0", "nbcid=" + data.params.nbcid);
}
// Get element references
ui.algorithm = document.getElementById(elements.algorithm);
ui.checkAttempts = document.getElementById(elements.checkAttempts);
ui.checkBody = document.getElementById(elements.checkBody);
ui.checkInterval = document.getElementById(elements.checkInterval);
ui.checkPath = document.getElementById(elements.checkPath);
ui.checkShow = document.getElementsByClassName(elements.checkShow);
ui.checkTimeout = document.getElementById(elements.checkTimeout);
ui.checkType = document.getElementById(elements.checkType);
ui.cipherSuite = document.getElementById(elements.cipherSuite);
ui.commonName = document.getElementById(elements.commonName);
ui.configLabel = document.getElementById(elements.configLabel);
ui.fingerprint = document.getElementById(elements.fingerprint);
ui.nodebalancerLabel = document.getElementById(elements.nodebalancerLabel);
ui.nodebalancerTag = document.getElementById(elements.nodebalancerTag);
ui.nodebalancerTagLink = document.getElementById(elements.nodebalancerTagLink);
ui.nodesTable = document.getElementById(elements.nodesTable);
ui.passive = document.getElementById(elements.passive);
ui.port = document.getElementById(elements.port);
ui.protocol = document.getElementById(elements.protocol);
ui.protocolShow = document.getElementsByClassName(elements.protocolShow);
ui.proxyProtocol = document.getElementById(elements.proxyProtocol);
ui.replaceLink = document.getElementById(elements.replaceLink);
ui.saveButton = document.getElementById(elements.saveButton);
ui.sslCert = document.getElementById(elements.sslCert);
ui.sslKey = document.getElementById(elements.sslKey);
ui.stickiness = document.getElementById(elements.stickiness);
// Register event handlers
ui.checkType.addEventListener("input", showHideChecks);
ui.protocol.addEventListener("input", showHideProtocol);
ui.replaceLink.addEventListener("click", handleReplace);
ui.saveButton.addEventListener("click", handleSave);
// Get data from API
apiGet("/nodebalancers/" + data.params.nbid, displayNodebalancer, null);
if (parseInt(data.params.nbcid)) {
apiGet("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid, displayConfig, null);
apiGet("/nodebalancers/" + data.params.nbid + "/configs/" + data.params.nbcid + "/nodes", displayNodes, null);
} else {
ui.configLabel.innerHTML = "Create Configuration";
ui.saveButton.disabled = false;
var hideElements = document.getElementsByClassName(elements.nonzero);
for (var i = 0; i < hideElements.length; i++)
hideElements[i].style.display = "none";
}
};
// Show/hide check rows
var showHideChecks = function(event)
{
for (var i = 0; i < ui.checkShow.length; i++) {
if (ui.checkShow[i].classList.contains(elements.checkShow + "-" + ui.checkType.value))
ui.checkShow[i].style.display = "table-row";
else
ui.checkShow[i].style.display = "none";
}
};
// Show/hide protocol stuff
var showHideProtocol = function(event)
{
for (var i = 0; i < ui.protocolShow.length; i++) {
if (ui.protocolShow[i].classList.contains(elements.protocolShow + "-" + ui.protocol.value)) {
if (ui.protocolShow[i].classList.contains(elements.replaceCert)) {
if (ui.protocolShow[i].classList.contains(elements.replaceCertOnly) == state.replaceCert)
ui.protocolShow[i].style.display = "table-row";
else
ui.protocolShow[i].style.display = "none";
} else {
ui.protocolShow[i].style.display = "table-row";
}
} else {
ui.protocolShow[i].style.display = "none";
}
}
if (data.params.nbcid == 0 && ui.protocol.value == "http")
ui.port.value = 80;
if (data.params.nbcid == 0 && ui.protocol.value == "https")
ui.port.value = 443;
};
// Attach onload handler
window.addEventListener("load", setup);
})();

View File

@ -0,0 +1,222 @@
<!--
This file is part of Linode Manager Classic.
Linode Manager Classic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Linode Manager Classic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Linode Manager Classic. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>LMC - NodeBalancers</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" type="text/css" href="config.css" />
<script src="config.js" type="module"></script>
</head>
<body>
<!--#include virtual="/include/header.html"-->
<div id="main-content" class="wrapper">
<div id="top-links"><a href="/nodebalancers">NodeBalancers</a> » <span id="nodebalancer-tag"><a id="nodebalancer-tag-link" href=""></a> » </span><a id="nodebalancer-label" href="/nodebalancers/balancer?nbid=0"></a> » <span id="config-label" class="top-links-title"></span></div>
<div id="config">
<table class="lmc-table nonzero">
<thead>
<tr>
<td colspan="7">Nodes</td>
</tr>
<tr>
<td>Label</td>
<td>Address</td>
<td>Port</td>
<td>Weight</td>
<td>Mode</td>
<td>Status</td>
<td class="center-cell">Options</td>
</tr>
</thead>
<tbody id="nodes-table"></tbody>
</table>
<p class="sub-links nonzero"><a href="/nodebalancers/node?nbid=0&nbcid=0&nbnid=0">Add Node</a></p>
<table id="config-table" class="lmc-table">
<thead>
<tr>
<td colspan="3">Edit Configuration</td>
</tr>
<tr>
<td colspan="3">Configuration Settings</td>
</tr>
</thead>
<tbody>
<tr class="lmc-tr3">
<td>Port</td>
<td><input id="port" type="number" min="1" max="65535" value="80" size="4" /></td>
<td class="info">Listen on this port</td>
</tr>
<tr class="lmc-tr3">
<td>Protocol</td>
<td>
<select id="protocol">
<option value="tcp">TCP</option>
<option selected value="http">HTTP</option>
<option value="https">HTTPS</option>
</select>
</td>
<td></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-tcp">
<td>Proxy Protocol</td>
<td>
<select id="proxy-protocol">
<option selected value="none">None</option>
<option value="v1">v1</option>
<option value="v2">v2</option>
</select>
</td>
<td class="info">Proxy protocol preserves initial TCP connection information. Consult the <a target="_blank" href="https://www.linode.com/docs/products/networking/nodebalancers/guides/proxy-protocol/">Proxy Protocol guide</a> for information on the differences between each option.</td>
</tr>
<tr class="lmc-tr3">
<td>Algorithm</td>
<td>
<select id="algorithm">
<option selected value="roundrobin">Round Robin</option>
<option value="leastconn">Least Connections</option>
<option value="source">Source IP</option>
</select>
</td>
<td class="info">Roundrobin assigns connections to each backend sequentially. Least connections choose the backend with the least number of current connections. Source uses the client's IPv4 address</td>
</tr>
<tr class="lmc-tr3">
<td>Session Stickiness</td>
<td>
<select id="stickiness">
<option value="none">None</option>
<option selected value="table">Table</option>
<option value="http_cookie">HTTP Cookie</option>
</select>
</td>
<td class="info">Route subsequent requests from a client to the same backend</td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr class="protocol-show protocol-show-https">
<td colspan="3">SSL Settings</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert">
<td>Common Name</td>
<td id="common-name"></td>
<td class="info">Need help with SSL? - <a target="_blank" href="https://www.linode.com/docs/products/networking/nodebalancers/guides/configure/">NodeBalancer Reference Guide</a></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert">
<td>Fingerprint</td>
<td id="fingerprint" colspan="2"></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https">
<td>SSL Cipher Suite</td>
<td>
<select id="cipher-suite">
<option selected value="recommended">Recommended</option>
<option value="legacy">Legacy</option>
</select>
</td>
<td class="info">Select 'recommended' to use only strong encryption. Select 'legacy' to enable weak encryption that supports older browsers. You really want to select 'recommended'. <a target="_blank" href="https://www.linode.com/docs/products/networking/nodebalancers/guides/configure/">NodeBalancer Reference Guide</a></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert">
<td></td>
<td><a id="replace-link" href="#">Replace Certificate</a></td>
<td></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert replace-cert-only">
<td>SSL Certificate</td>
<td colspan="2"><textarea id="ssl-cert" rows="6" cols="64" placeholder="Please provide your SSL certificate (including chained intermediate certificates if needed)"></textarea></td>
</tr>
<tr class="lmc-tr3 protocol-show protocol-show-https replace-cert replace-cert-only">
<td>Private Key</td>
<td colspan="2"><textarea id="ssl-key" rows="6" cols="64" placeholder="Please provide your unpassphrased SSL private key"></textarea></td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3">Active Health Check</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3">
<td>Health Check Type</td>
<td>
<select id="check-type">
<option selected value="none">None</option>
<option value="connection">TCP Connection</option>
<option value="http">HTTP Valid Status</option>
<option value="http_body">HTTP Body Regex</option>
</select>
</td>
<td class="info">Active health checks proactively check the health of the backend nodes. 'TCP Connection' requires a successful TCP handshake. 'HTTP Valid Status' requires a 2xx or 3xx response from the backend node. 'HTTP Body Regex' uses a regex to match against an expected result body.</td>
</tr>
<tr class="lmc-tr3 check-show check-show-connection check-show-http check-show-http_body">
<td>Check Interval</td>
<td><input id="check-interval" type="number" min="1" value="5" size="4" /></td>
<td class="info">Seconds between health check probes</td>
</tr>
<tr class="lmc-tr3 check-show check-show-connection check-show-http check-show-http_body">
<td>Check Timeout</td>
<td><input id="check-timeout" type="number" min="1" max="30" value="3" size="4" /></td>
<td class="info">Seconds to wait before considering the probe a failure. 1-30. Must be less than check interval.</td>
</tr>
<tr class="lmc-tr3 check-show check-show-connection check-show-http check-show-http_body">
<td>Check Attempts</td>
<td><input id="check-attempts" type="number" min="1" max="30" value="2" size="4" /></td>
<td class="info">Number of failed probes before taking a node out of rotation. 1-30</td>
</tr>
<tr class="lmc-tr3 check-show check-show-http check-show-http_body">
<td>Check HTTP Path</td>
<td><input id="check-path" type="text" value="/" size="24" /></td>
<td class="info">When check=http, the path to request</td>
</tr>
<tr class="lmc-tr3 check-show check-show-http_body">
<td>Expected HTTP Body</td>
<td><textarea id="check-body" rows="4" cols="40"></textarea></td>
<td class="info">When check=http, a regex to match within the first 16KB of the response body</td>
</tr>
</tbody>
<tbody class="lmc-tbody-head">
<tr class="noshow">
<td colspan="3"></td>
</tr>
<tr>
<td colspan="3">Passive Checks</td>
</tr>
</tbody>
<tbody>
<tr class="lmc-tr3">
<td>Enabled</td>
<td><input checked id="passive" type="checkbox" /></td>
<td class="info">Enable passive checks based on observing communication with backend nodes.</td>
</tr>
<tr class="lmc-tr3">
<td></td>
<td><button disabled id="save-button" type="button">Save Changes</button></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>