Cloud Subnet Sizing (#11)
* Feature/cloud (#8) * calc for cloud platforms * updates after further testing * remove debugging statements --------- Co-authored-by: Caesar Kabalan <caesar.kabalan@gmail.com> Co-authored-by: Byron Collins <byron_collins@hotmail.com> * refactor cloud subnet sizes based on feedback * added missing integrity SRI Hashes * update set_popover_content * Modify description to highlight AWS and Azure capabilities. --------- Co-authored-by: Caesar Kabalan <caesar.kabalan@gmail.com> Co-authored-by: Byron Collins <byron_collins@hotmail.com>
This commit is contained in:
parent
6b054e9bb6
commit
6f55ba52ec
8 changed files with 364 additions and 53 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
.idea/*
|
.idea/*
|
||||||
**/node_modules/*
|
**/node_modules/*
|
||||||
|
**/certs/*
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
v18.16.0
|
v20.12.2
|
||||||
|
|
51
README.md
51
README.md
|
@ -45,8 +45,59 @@ Compile from source:
|
||||||
> npm start
|
> npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The full application should then be available within `./dist/`, open `./dist/index.html` in a browser.
|
The full application should then be available within `./dist/`, open `./dist/index.html` in a browser.
|
||||||
|
|
||||||
|
### Run with certificates (Optional)
|
||||||
|
|
||||||
|
***NB:*** *required for testing clipboard.writeText() in the browser. Feature is only available in secure (https) mode.*
|
||||||
|
|
||||||
|
```shell
|
||||||
|
|
||||||
|
#Install mkcert
|
||||||
|
> brew install mkcert
|
||||||
|
# generate CA Certs to be trusted by local browsers
|
||||||
|
> mkcert install
|
||||||
|
# generate certs for local development
|
||||||
|
> cd visualsubnetcalc/src
|
||||||
|
# generate certs for local development
|
||||||
|
> npm run setup:certs
|
||||||
|
# run the local webserver with https
|
||||||
|
> npm run local-secure-start
|
||||||
|
````
|
||||||
|
|
||||||
|
# Cloud Subnet Notes
|
||||||
|
|
||||||
|
- [AWS reserves 3 additional IPs](https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html)
|
||||||
|
|
||||||
|
- [Azure reserves 3 additional IPs](https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets)
|
||||||
|
|
||||||
|
|
||||||
|
## Standard mode:
|
||||||
|
- Smallest subnet: /32
|
||||||
|
- Two reserved addresses per subnet of size <= 30:
|
||||||
|
- Network Address (network + 0)
|
||||||
|
- Broadcast Address (last network address)
|
||||||
|
## AWS mode :
|
||||||
|
- Smallest subnet: /28
|
||||||
|
- Five reserved addresses per subnet:
|
||||||
|
- Network Address (network + 0)
|
||||||
|
- AWS Reserved - VPC Router
|
||||||
|
- AWS Reserved - VPC DNS
|
||||||
|
- AWS Reserved - Future Use
|
||||||
|
- Broadcast Address (last network address)
|
||||||
|
## Azure mode :
|
||||||
|
- Smallest subnet: /29
|
||||||
|
- Five reserved addresses per subnet:
|
||||||
|
- Network Address (network + 0)
|
||||||
|
- Azure Reserved - Default Gateway
|
||||||
|
- Azure Reserved - DNS Mapping
|
||||||
|
- Azure Reserved - DNS Mapping
|
||||||
|
- Broadcast Address (last network address)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
Split icon made by [Freepik](https://www.flaticon.com/authors/freepik) from [Flaticon](https://www.flaticon.com/).
|
Split icon made by [Freepik](https://www.flaticon.com/authors/freepik) from [Flaticon](https://www.flaticon.com/).
|
||||||
|
|
5
dist/css/main.css
vendored
5
dist/css/main.css
vendored
|
@ -255,3 +255,8 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-mode {
|
||||||
|
color: #4091C9;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
35
dist/index.html
vendored
35
dist/index.html
vendored
|
@ -6,7 +6,7 @@
|
||||||
<title>Visual Subnet Calculator - Split/Join</title>
|
<title>Visual Subnet Calculator - Split/Join</title>
|
||||||
<link rel="stylesheet" href="css/bootstrap.min.css">
|
<link rel="stylesheet" href="css/bootstrap.min.css">
|
||||||
<link href="css/main.css" rel="stylesheet">
|
<link href="css/main.css" rel="stylesheet">
|
||||||
<meta name="description" content="Quickly and easily design network layouts. Split and join subnets, add notes and color, then collaborate with others by sharing a custom link to your design.">
|
<meta name="description" content="Quickly and easily design network layouts. Split and join subnets, add notes and color, then collaborate with others by sharing a custom link to your design. Switch to AWS or Azure Mode to calculate compliant subnets for those Cloud Platforms">
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="icon/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="icon/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="icon/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="icon/favicon-32x32.png">
|
||||||
|
@ -36,11 +36,10 @@
|
||||||
<p class="mb-0">Enter the network you wish to subnet and use the Split/Join buttons on the right to start designing!</p>
|
<p class="mb-0">Enter the network you wish to subnet and use the Split/Join buttons on the right to start designing!</p>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<form id="input_form" class="row g-2 mb-3">
|
||||||
<form id="input_form" class="row g-2 mb-3" novalidate>
|
|
||||||
<div class="font-monospace col-lg-2 col-md-3 col-4">
|
<div class="font-monospace col-lg-2 col-md-3 col-4">
|
||||||
<div><label for="network" class="form-label mb-0 ms-1">Network Address</label></div>
|
<div><label for="network" class="form-label mb-0 ms-1">Network Address</label></div>
|
||||||
<div><input id="network" type="text" class="form-control" value="10.0.0.0" aria-label="Network Address" pattern="^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" required></div>
|
<div><input name="network" id="network" type="text" class="form-control" value="10.0.0.0" aria-label="Network Address" pattern="^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" required></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-monospace col-auto">
|
<div class="font-monospace col-auto">
|
||||||
<div style="height:2rem"></div>
|
<div style="height:2rem"></div>
|
||||||
|
@ -48,7 +47,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="font-monospace col-lg-2 col-md-3 col-4">
|
<div class="font-monospace col-lg-2 col-md-3 col-4">
|
||||||
<div><label for="netsize" class="form-label mb-0 ms-1">Network Size</label></div>
|
<div><label for="netsize" class="form-label mb-0 ms-1">Network Size</label></div>
|
||||||
<div><input id="netsize" type="text" class="form-control w-10" value="16" aria-label="Network Size" pattern="^(\d|[12]\d|30)$" required></div>
|
<div><input name="netsize" id="netsize" type="text" class="form-control w-10" value="16" aria-label="Network Size" pattern="^([0-9]|[12][0-9]|3[0-2])$" required></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2 col-md-3 col-3 font-">
|
<div class="col-lg-2 col-md-3 col-3 font-">
|
||||||
<div style="height:1.5rem"></div>
|
<div style="height:1.5rem"></div>
|
||||||
|
@ -60,6 +59,11 @@
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importExportModal" id="btn_import_export">Import / Export</a></li>
|
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importExportModal" id="btn_import_export">Import / Export</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><h6 class="dropdown-header">Mode</h6></li>
|
||||||
|
<li><a class="dropdown-item active-mode" href="#" data-bs-toggle="operatingMode" data-bs-target="#operatingMode" id="dropdown_standard">Standard</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" data-bs-toggle="operatingMode" data-bs-target="#operatingMode" id="dropdown_azure">Azure</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" data-bs-toggle="operatingMode" data-bs-target="#operatingMode" id="dropdown_aws">AWS</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,17 +71,18 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table id="calc" class="table table-bordered font-monospace">
|
<table id="calc" class="table table-bordered font-monospace">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th id="subnetHeader" style="display: table-cell;">Subnet address</th>
|
<th id="subnetHeader" style="display: table-cell;">Subnet address</th>
|
||||||
<th id="netmaskHeader" style="display: none;">Netmask</th>
|
<th id="netmaskHeader" style="display: none;">Netmask</th>
|
||||||
<th id="rangeHeader" style="display: table-cell;">Range of addresses</th>
|
<th id="rangeHeader" style="display: table-cell;">Range of addresses</th>
|
||||||
<th id="useableHeader" style="display: table-cell;">Usable IPs</th>
|
<th id="useableHeader" style="display: table-cell;" data-bs-toggle="popover" title="Usable IPs" data-bs-content="This column shows the number of usable IP addresses in each subnet." data-bs-trigger="hover focus" data-bs-placement="top">Usable IPs</th>
|
||||||
<th id="hostsHeader" style="display: table-cell;">Hosts</th>
|
<th id="hostsHeader" style="display: table-cell;">Hosts</th>
|
||||||
<th id="noteHeader" colspan="100%" style="display: table-cell;">
|
<th id="noteHeader" colspan="100%" style="display: table-cell;">
|
||||||
Note
|
Note
|
||||||
<div style="display:inline-block; float:right;"><span class="split">Split</span>/<span class="join">Join</span></div>
|
<div style="float:right;"><span class="split">Split</span>/<span class="join">Join</span></div>
|
||||||
</th>
|
</th>
|
||||||
<!--
|
<!--
|
||||||
<th id="joinHeader" colspan="100%" style="display: table-cell;"><span class="split">Split</span>/<span class="join">Join</span></th>
|
<th id="joinHeader" colspan="100%" style="display: table-cell;"><span class="split">Split</span>/<span class="join">Join</span></th>
|
||||||
|
@ -191,14 +196,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe"
|
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"
|
<script src="https://code.jquery.com/jquery-3.7.0.min.js"
|
||||||
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
|
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.19.5/dist/jquery.validate.min.js"
|
||||||
|
integrity="sha384-aEDtD4n2FLrMdE9psop0SHdNyy/W9cBjH22rSRp+3wPHd62Y32uijc0H2eLmgaSn"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.19.5/dist/additional-methods.min.js"
|
||||||
|
integrity="sha384-kxI94MBt6egf2HqINJ9x8sPSfqxudviPxgttYjlTFaPREJb/gmAOgTr/GYie5ung"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
<script src="js/lz-string.min.js"></script>
|
<script src="js/lz-string.min.js"></script>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
|
<script>
|
||||||
|
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
|
||||||
|
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||||
|
return new bootstrap.Popover(popoverTriggerEl)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
283
dist/js/main.js
vendored
283
dist/js/main.js
vendored
|
@ -7,7 +7,7 @@ let infoColumnCount = 5
|
||||||
// - Two reserved addresses per subnet of size <= 30:
|
// - Two reserved addresses per subnet of size <= 30:
|
||||||
// - Network Address (network + 0)
|
// - Network Address (network + 0)
|
||||||
// - Broadcast Address (last network address)
|
// - Broadcast Address (last network address)
|
||||||
// AWS mode (future):
|
// AWS mode :
|
||||||
// - Smallest subnet: /28
|
// - Smallest subnet: /28
|
||||||
// - Two reserved addresses per subnet:
|
// - Two reserved addresses per subnet:
|
||||||
// - Network Address (network + 0)
|
// - Network Address (network + 0)
|
||||||
|
@ -15,17 +15,39 @@ let infoColumnCount = 5
|
||||||
// - AWS Reserved - VPC DNS
|
// - AWS Reserved - VPC DNS
|
||||||
// - AWS Reserved - Future Use
|
// - AWS Reserved - Future Use
|
||||||
// - Broadcast Address (last network address)
|
// - Broadcast Address (last network address)
|
||||||
let operatingMode = 'NORMAL'
|
// Azure mode :
|
||||||
|
// - Smallest subnet: /29
|
||||||
|
// - Two reserved addresses per subnet:
|
||||||
|
// - Network Address (network + 0)
|
||||||
|
// - Azure Reserved - Default Gateway
|
||||||
|
// - Azure Reserved - DNS Mapping
|
||||||
|
// - Azure Reserved - DNS Mapping
|
||||||
|
// - Broadcast Address (last network address)
|
||||||
let noteTimeout;
|
let noteTimeout;
|
||||||
let minSubnetSize = 32
|
let operatingMode = 'Standard'
|
||||||
|
let previousOperatingMode = 'Standard'
|
||||||
let inflightColor = 'NONE'
|
let inflightColor = 'NONE'
|
||||||
let urlVersion = '1'
|
let urlVersion = '2'
|
||||||
let configVersion = '1'
|
let configVersion = '2'
|
||||||
|
|
||||||
|
const netsizePatterns = {
|
||||||
|
Standard: '^([0-9]|[12][0-9]|3[0-2])$',
|
||||||
|
AZURE: '^([0-9]|[12][0-9])$',
|
||||||
|
AWS: '^([0-9]|[12][0-8])$',
|
||||||
|
};
|
||||||
|
|
||||||
|
const minSubnetSizes = {
|
||||||
|
Standard: 32,
|
||||||
|
AZURE: 29,
|
||||||
|
AWS: 28,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
$('input#network,input#netsize').on('input', function() {
|
$('input#network,input#netsize').on('input', function() {
|
||||||
$('#input_form')[0].classList.add('was-validated');
|
$('#input_form')[0].classList.add('was-validated');
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
$('#color_palette div').on('click', function() {
|
$('#color_palette div').on('click', function() {
|
||||||
// We don't really NEED to convert this to hex, but it's really low overhead to do the
|
// We don't really NEED to convert this to hex, but it's really low overhead to do the
|
||||||
// conversion here and saves us space in the export/save
|
// conversion here and saves us space in the export/save
|
||||||
|
@ -42,9 +64,51 @@ $('#calcbody').on('click', '.row_address, .row_range, .row_usable, .row_hosts, .
|
||||||
})
|
})
|
||||||
|
|
||||||
$('#btn_go').on('click', function() {
|
$('#btn_go').on('click', function() {
|
||||||
|
$('#input_form').removeClass('was-validated');
|
||||||
|
$('#input_form').validate();
|
||||||
|
if ($('#input_form').valid()) {
|
||||||
|
$('#input_form')[0].classList.add('was-validated');
|
||||||
reset();
|
reset();
|
||||||
|
// Additional actions upon validation can be added here
|
||||||
|
} else {
|
||||||
|
show_warning_modal('<div>Please correct the errors in the form!</div>');
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$('#dropdown_standard').click(function() {
|
||||||
|
previousOperatingMode = operatingMode;
|
||||||
|
operatingMode = 'Standard';
|
||||||
|
|
||||||
|
if(!switchMode(operatingMode)) {
|
||||||
|
operatingMode = previousOperatingMode;
|
||||||
|
$('#dropdown_'+ operatingMode.toLowerCase()).addClass('active-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#dropdown_azure').click(function() {
|
||||||
|
previousOperatingMode = operatingMode;
|
||||||
|
operatingMode = 'AZURE';
|
||||||
|
|
||||||
|
if(!switchMode(operatingMode)) {
|
||||||
|
operatingMode = previousOperatingMode;
|
||||||
|
$('#dropdown_'+ operatingMode.toLowerCase()).addClass('active-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#dropdown_aws').click(function() {
|
||||||
|
previousOperatingMode = operatingMode;
|
||||||
|
operatingMode = 'AWS';
|
||||||
|
|
||||||
|
if(!switchMode(operatingMode)) {
|
||||||
|
operatingMode = previousOperatingMode;
|
||||||
|
$('#dropdown_'+ operatingMode.toLowerCase()).addClass('active-mode');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
$('#importBtn').on('click', function() {
|
$('#importBtn').on('click', function() {
|
||||||
importConfig(JSON.parse($('#importExportArea').val()))
|
importConfig(JSON.parse($('#importExportArea').val()))
|
||||||
})
|
})
|
||||||
|
@ -73,17 +137,14 @@ $('#bottom_nav #copy_url').on('click', function() {
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
$('#btn_import_export').on('click', function() {
|
$('#btn_import_export').on('click', function() {
|
||||||
$('#importExportArea').val(JSON.stringify(exportConfig(), null, 2))
|
$('#importExportArea').val(JSON.stringify(exportConfig(), null, 2))
|
||||||
})
|
})
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
if (operatingMode === 'AWS') {
|
|
||||||
minSubnetSize = 28
|
set_popover_content(operatingMode);
|
||||||
} else {
|
|
||||||
minSubnetSize = 32
|
|
||||||
}
|
|
||||||
let cidrInput = $('#network').val() + '/' + $('#netsize').val()
|
let cidrInput = $('#network').val() + '/' + $('#netsize').val()
|
||||||
let rootNetwork = get_network($('#network').val(), $('#netsize').val())
|
let rootNetwork = get_network($('#network').val(), $('#netsize').val())
|
||||||
let rootCidr = rootNetwork + '/' + $('#netsize').val()
|
let rootCidr = rootNetwork + '/' + $('#netsize').val()
|
||||||
|
@ -94,13 +155,13 @@ function reset() {
|
||||||
subnetMap = {}
|
subnetMap = {}
|
||||||
subnetMap[rootCidr] = {}
|
subnetMap[rootCidr] = {}
|
||||||
maxNetSize = parseInt($('#netsize').val())
|
maxNetSize = parseInt($('#netsize').val())
|
||||||
renderTable();
|
renderTable(operatingMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#calcbody').on('click', 'td.split,td.join', function(event) {
|
$('#calcbody').on('click', 'td.split,td.join', function(event) {
|
||||||
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
|
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
|
||||||
mutate_subnet_map(this.dataset.mutateVerb, this.dataset.subnet, '')
|
mutate_subnet_map(this.dataset.mutateVerb, this.dataset.subnet, '')
|
||||||
renderTable();
|
renderTable(operatingMode);
|
||||||
})
|
})
|
||||||
|
|
||||||
$('#calcbody').on('keyup', 'td.note input', function(event) {
|
$('#calcbody').on('keyup', 'td.note input', function(event) {
|
||||||
|
@ -119,18 +180,18 @@ $('#calcbody').on('focusout', 'td.note input', function(event) {
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function renderTable() {
|
function renderTable(operatingMode) {
|
||||||
// TODO: Validation Code
|
// TODO: Validation Code
|
||||||
$('#calcbody').empty();
|
$('#calcbody').empty();
|
||||||
let maxDepth = get_dict_max_depth(subnetMap, 0)
|
let maxDepth = get_dict_max_depth(subnetMap, 0)
|
||||||
addRowTree(subnetMap, 0, maxDepth)
|
addRowTree(subnetMap, 0, maxDepth, operatingMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRowTree(subnetTree, depth, maxDepth) {
|
function addRowTree(subnetTree, depth, maxDepth,operatingMode) {
|
||||||
for (let mapKey in subnetTree) {
|
for (let mapKey in subnetTree) {
|
||||||
if (mapKey.startsWith('_')) { continue; }
|
if (mapKey.startsWith('_')) { continue; }
|
||||||
if (has_network_sub_keys(subnetTree[mapKey])) {
|
if (has_network_sub_keys(subnetTree[mapKey])) {
|
||||||
addRowTree(subnetTree[mapKey], depth + 1, maxDepth)
|
addRowTree(subnetTree[mapKey], depth + 1, maxDepth,operatingMode)
|
||||||
} else {
|
} else {
|
||||||
let subnet_split = mapKey.split('/')
|
let subnet_split = mapKey.split('/')
|
||||||
let notesWidth = '30%';
|
let notesWidth = '30%';
|
||||||
|
@ -143,12 +204,12 @@ function addRowTree(subnetTree, depth, maxDepth) {
|
||||||
} else if (maxDepth > 20) {
|
} else if (maxDepth > 20) {
|
||||||
notesWidth = '10%';
|
notesWidth = '10%';
|
||||||
}
|
}
|
||||||
addRow(subnet_split[0], parseInt(subnet_split[1]), (infoColumnCount + maxDepth - depth), (subnetTree[mapKey]['_note'] || ''), notesWidth, (subnetTree[mapKey]['_color'] || ''))
|
addRow(subnet_split[0], parseInt(subnet_split[1]), (infoColumnCount + maxDepth - depth), (subnetTree[mapKey]['_note'] || ''), notesWidth, (subnetTree[mapKey]['_color'] || ''),operatingMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRow(network, netSize, colspan, note, notesWidth, color) {
|
function addRow(network, netSize, colspan, note, notesWidth, color, operatingMode) {
|
||||||
let addressFirst = ip2int(network)
|
let addressFirst = ip2int(network)
|
||||||
let addressLast = subnet_last_address(addressFirst, netSize)
|
let addressLast = subnet_last_address(addressFirst, netSize)
|
||||||
let usableFirst = subnet_usable_first(addressFirst, netSize, operatingMode)
|
let usableFirst = subnet_usable_first(addressFirst, netSize, operatingMode)
|
||||||
|
@ -216,7 +277,9 @@ function subnet_usable_first(network, netSize, operatingMode) {
|
||||||
if (netSize < 31) {
|
if (netSize < 31) {
|
||||||
// https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html
|
// https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html
|
||||||
// AWS reserves 3 additional IPs
|
// AWS reserves 3 additional IPs
|
||||||
return network + (operatingMode === 'AWS' ? 4 : 1);
|
// https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets
|
||||||
|
// Azure reserves 3 additional IPs
|
||||||
|
return network + (operatingMode == 'Standard' ? 1 : 4);
|
||||||
} else {
|
} else {
|
||||||
return network;
|
return network;
|
||||||
}
|
}
|
||||||
|
@ -362,7 +425,7 @@ function mutate_subnet_map(verb, network, subnetTree, propValue = '') {
|
||||||
let netSplit = mapKey.split('/')
|
let netSplit = mapKey.split('/')
|
||||||
let netSize = parseInt(netSplit[1])
|
let netSize = parseInt(netSplit[1])
|
||||||
if (verb === 'split') {
|
if (verb === 'split') {
|
||||||
if (netSize < minSubnetSize) {
|
if (netSize < minSubnetSizes[operatingMode]) {
|
||||||
let new_networks = split_network(netSplit[0], netSize)
|
let new_networks = split_network(netSplit[0], netSize)
|
||||||
// Could maybe optimize this for readability with some null coalescing
|
// Could maybe optimize this for readability with some null coalescing
|
||||||
subnetTree[mapKey][new_networks[0]] = {}
|
subnetTree[mapKey][new_networks[0]] = {}
|
||||||
|
@ -380,6 +443,16 @@ function mutate_subnet_map(verb, network, subnetTree, propValue = '') {
|
||||||
subnetTree[mapKey][new_networks[1]]['_color'] = subnetTree[mapKey]['_color']
|
subnetTree[mapKey][new_networks[1]]['_color'] = subnetTree[mapKey]['_color']
|
||||||
}
|
}
|
||||||
delete subnetTree[mapKey]['_color']
|
delete subnetTree[mapKey]['_color']
|
||||||
|
} else {
|
||||||
|
switch (operatingMode) {
|
||||||
|
case 'AWS':
|
||||||
|
case 'AZURE':
|
||||||
|
show_warning_modal('<div>Minimum subnet size for ' + operatingMode + ' is ' + minSubnetSizes[operatingMode] + '</div>')
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
show_warning_modal('<div>Minimum subnet size is ' + minSubnetSizes[operatingMode] + '</div>')
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (verb === 'join') {
|
} else if (verb === 'join') {
|
||||||
// Options:
|
// Options:
|
||||||
|
@ -402,6 +475,112 @@ function mutate_subnet_map(verb, network, subnetTree, propValue = '') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchMode(operatingMode) {
|
||||||
|
|
||||||
|
let isSwitched = true;
|
||||||
|
|
||||||
|
if (subnetMap !== null) {
|
||||||
|
if (validateSubnetSizes(subnetMap, minSubnetSizes[operatingMode])) {
|
||||||
|
|
||||||
|
renderTable(operatingMode);
|
||||||
|
set_popover_content(operatingMode);
|
||||||
|
|
||||||
|
$('#netsize').attr("pattern",netsizePatterns[operatingMode]);
|
||||||
|
$('#input_form').removeClass('was-validated');
|
||||||
|
$('#input_form').rules("remove", "netsize");
|
||||||
|
|
||||||
|
switch (operatingMode) {
|
||||||
|
case 'AWS':
|
||||||
|
case 'AZURE':
|
||||||
|
var message = "("+operatingMode+") Smallest size is " + minSubnetSizes[operatingMode]
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
var message = "Smallest size is " + minSubnetSizes[operatingMode]
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Modify jquery validation rule
|
||||||
|
$('#input_form #netsize').rules("add", {
|
||||||
|
required: true,
|
||||||
|
pattern: netsizePatterns[operatingMode],
|
||||||
|
messages: {
|
||||||
|
required: "Please enter a network size",
|
||||||
|
pattern: message
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Remove active class from all buttons if needed
|
||||||
|
$('#dropdown_standard, #dropdown_azure, #dropdown_aws').removeClass('active-mode');
|
||||||
|
$('#dropdown_' + operatingMode.toLowerCase()).addClass('active-mode');
|
||||||
|
isSwitched = true;
|
||||||
|
} else {
|
||||||
|
show_warning_modal('<div>Some subnets have a netmask size smaller than the minimum allowed for ' + operatingMode +'.</div><div>The smallest size allowed is ' + minSubnetSizes[operatingMode] + '</div>');
|
||||||
|
isSwitched = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//unlikely to get here.
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSwitched;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSubnetSizes(subnetMap, minSubnetSize) {
|
||||||
|
let isValid = true;
|
||||||
|
const validate = (subnetTree) => {
|
||||||
|
for (let key in subnetTree) {
|
||||||
|
if (key.startsWith('_')) continue; // Skip special keys
|
||||||
|
let [_, size] = key.split('/');
|
||||||
|
if (parseInt(size) > minSubnetSize) {
|
||||||
|
isValid = false;
|
||||||
|
return; // Early exit if any subnet is invalid
|
||||||
|
}
|
||||||
|
if (typeof subnetTree[key] === 'object') {
|
||||||
|
validate(subnetTree[key]); // Recursively validate subnets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
validate(subnetMap);
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function set_popover_content(operatingMode) {
|
||||||
|
var popoverContent = "This column shows the number of usable IP addresses in each subnet.";
|
||||||
|
var popoverTitle = "Usable IPs"
|
||||||
|
|
||||||
|
switch (operatingMode) {
|
||||||
|
case 'AWS':
|
||||||
|
case 'AZURE':
|
||||||
|
var popoverTitle = "Usable IPs (" + operatingMode + ")";
|
||||||
|
var popoverContent = "This column shows the number of usable IP addresses in each subnet. " + operatingMode + " reserves 5 IP Addresses"
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
var popoverContent = "This column shows the number of usable IP addresses in each subnet.";
|
||||||
|
var popoverTitle = "Usable IPs"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the popover is properly disposed
|
||||||
|
$('#useableHeader').popover('dispose');
|
||||||
|
|
||||||
|
// Reinitialize the popover with direct options for title and content
|
||||||
|
$('#useableHeader').popover({
|
||||||
|
trigger: 'hover',
|
||||||
|
html: true,
|
||||||
|
title: function() {
|
||||||
|
// You can compute or fetch the title dynamically here
|
||||||
|
return popoverTitle;
|
||||||
|
},
|
||||||
|
content: function() {
|
||||||
|
// You can compute or fetch the content dynamically here
|
||||||
|
return popoverContent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function show_warning_modal(message) {
|
function show_warning_modal(message) {
|
||||||
var notifyModal = new bootstrap.Modal(document.getElementById("notifyModal"), {});
|
var notifyModal = new bootstrap.Modal(document.getElementById("notifyModal"), {});
|
||||||
|
@ -410,6 +589,39 @@ function show_warning_modal(message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$( document ).ready(function() {
|
$( document ).ready(function() {
|
||||||
|
|
||||||
|
// Initialize the jQuery Validation on the form
|
||||||
|
var validator = $('#input_form').validate({
|
||||||
|
rules: {
|
||||||
|
network: {
|
||||||
|
required: true,
|
||||||
|
pattern: "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
|
||||||
|
},
|
||||||
|
netsize: {
|
||||||
|
required: true,
|
||||||
|
pattern: "^([0-9]|[12][0-9]|3[0-2])$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
network: {
|
||||||
|
required: "Please enter a network",
|
||||||
|
pattern: "Must be a valid IP Address"
|
||||||
|
},
|
||||||
|
netsize: {
|
||||||
|
required: "Please enter a network size",
|
||||||
|
pattern: "Smallest size is 32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorPlacement: function(error, element) {
|
||||||
|
error.insertAfter(element);
|
||||||
|
},
|
||||||
|
// When the form is valid, add the 'was-validated' class
|
||||||
|
submitHandler: function(form) {
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
form.submit(); // Submit the form
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let autoConfigResult = processConfigUrl();
|
let autoConfigResult = processConfigUrl();
|
||||||
if (!autoConfigResult) {
|
if (!autoConfigResult) {
|
||||||
reset();
|
reset();
|
||||||
|
@ -421,6 +633,7 @@ $( document ).ready(function() {
|
||||||
function exportConfig() {
|
function exportConfig() {
|
||||||
return {
|
return {
|
||||||
'config_version': configVersion,
|
'config_version': configVersion,
|
||||||
|
'operating_mode': operatingMode,
|
||||||
'subnets': subnetMap,
|
'subnets': subnetMap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -428,6 +641,7 @@ function exportConfig() {
|
||||||
function getConfigUrl() {
|
function getConfigUrl() {
|
||||||
let defaultExport = JSON.parse(JSON.stringify(exportConfig()));
|
let defaultExport = JSON.parse(JSON.stringify(exportConfig()));
|
||||||
renameKey(defaultExport, 'config_version', 'v')
|
renameKey(defaultExport, 'config_version', 'v')
|
||||||
|
renameKey(defaultExport, 'operating_mode', 'm')
|
||||||
renameKey(defaultExport, 'subnets', 's')
|
renameKey(defaultExport, 'subnets', 's')
|
||||||
shortenKeys(defaultExport['s'])
|
shortenKeys(defaultExport['s'])
|
||||||
return '/index.html?c=' + urlVersion + LZString.compressToEncodedURIComponent(JSON.stringify(defaultExport))
|
return '/index.html?c=' + urlVersion + LZString.compressToEncodedURIComponent(JSON.stringify(defaultExport))
|
||||||
|
@ -441,14 +655,18 @@ function processConfigUrl() {
|
||||||
// First character is the version of the URL string, in case the mechanism of encoding changes
|
// First character is the version of the URL string, in case the mechanism of encoding changes
|
||||||
let urlVersion = params['c'].substring(0, 1)
|
let urlVersion = params['c'].substring(0, 1)
|
||||||
let urlData = params['c'].substring(1)
|
let urlData = params['c'].substring(1)
|
||||||
if (urlVersion === '1') {
|
|
||||||
let urlConfig = JSON.parse(LZString.decompressFromEncodedURIComponent(params['c'].substring(1)))
|
let urlConfig = JSON.parse(LZString.decompressFromEncodedURIComponent(params['c'].substring(1)))
|
||||||
|
|
||||||
|
if (urlVersion === '2') {
|
||||||
|
renameKey(urlConfig, 'm','operating_mode')
|
||||||
|
}
|
||||||
|
|
||||||
renameKey(urlConfig, 'v', 'config_version')
|
renameKey(urlConfig, 'v', 'config_version')
|
||||||
renameKey(urlConfig, 's', 'subnets')
|
renameKey(urlConfig, 's', 'subnets')
|
||||||
expandKeys(urlConfig['subnets'])
|
expandKeys(urlConfig['subnets'])
|
||||||
importConfig(urlConfig)
|
importConfig(urlConfig)
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,14 +718,25 @@ function renameKey(obj, oldKey, newKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function importConfig(text) {
|
function importConfig(text) {
|
||||||
// TODO: Probably need error checking here
|
switch (text['config_version']) {
|
||||||
if (text['config_version'] === '1') {
|
case '1':
|
||||||
|
operatingMode = 'Standard';
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
operatingMode = text['operating_mode'];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Optionally handle unexpected config_version values
|
||||||
|
show_warning_modal('<div>Invalid operating_mode</div>');
|
||||||
|
reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
let subnet_split = Object.keys(text['subnets'])[0].split('/')
|
let subnet_split = Object.keys(text['subnets'])[0].split('/')
|
||||||
$('#network').val(subnet_split[0])
|
$('#network').val(subnet_split[0])
|
||||||
$('#netsize').val(subnet_split[1])
|
$('#netsize').val(subnet_split[1])
|
||||||
subnetMap = text['subnets'];
|
subnetMap = text['subnets'];
|
||||||
renderTable()
|
switchMode(operatingMode);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}`
|
const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}`
|
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "visualsubnetcalc",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
|
@ -1,12 +1,14 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"lz-string": "^1.5.0",
|
"http-server": "^14.1.1",
|
||||||
"http-server": "^14.1.1"
|
"lz-string": "^1.5.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "npm install -g sass",
|
"postinstall": "sudo npm install -g sass",
|
||||||
"build": "sass --style compressed scss/custom.scss:../dist/css/bootstrap.min.css && cp node_modules/lz-string/libs/lz-string.min.js ../dist/js/lz-string.min.js",
|
"build": "sass --style compressed scss/custom.scss:../dist/css/bootstrap.min.css && cp node_modules/lz-string/libs/lz-string.min.js ../dist/js/lz-string.min.js",
|
||||||
"start": "node node_modules/http-server/bin/http-server ../dist -c-1"
|
"setup:certs": "mkdir -p certs; mkcert -cert-file certs/cert.pem -key-file certs/cert.key localhost 127.0.0.1",
|
||||||
|
"start": "node node_modules/http-server/bin/http-server ../dist -c-1",
|
||||||
|
"local-secure-start": "node node_modules/http-server/bin/http-server ../dist -c-1 -C certs/cert.pem -K certs/cert.key -S -p 8443"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue