This commit is contained in:
Matthew Garrett 2023-01-03 00:46:48 -08:00
Родитель 03d08cd68e
Коммит 11c0fec6de
8 изменённых файлов: 702 добавлений и 2 удалений

Просмотреть файл

@ -72,12 +72,16 @@ import Peering from "../../img/Peering";
// import Conflict from "../../img/Conflict";
import Person from "../../img/Person";
import Rule from "../../img/Rule";
import Tools from "../../img/Tools";
import Planner from "../../img/Planner";
import UserSettings from "./userSettings";
import Welcome from "../welcome/Welcome";
import DiscoverTabs from "../tabs/discoverTabs";
import AnalyzeTabs from "../tabs/analyzeTabs";
import ToolsTabs from "../tabs/toolsTabs";
import AdminTabs from "../tabs/adminTabs";
import ConfigureIPAM from "../configure/configure";
@ -140,7 +144,7 @@ export default function NavDrawer() {
icon: Home,
link: "/",
admin: false
},
}
],
[
{
@ -199,6 +203,19 @@ export default function NavDrawer() {
}
]
},
{
title: "Tools",
icon: Tools,
admin: false,
children: [
{
title: "Planner",
icon: Planner,
link: "tools/planner",
admin: false
}
]
}
],
[
{
@ -225,7 +242,7 @@ export default function NavDrawer() {
admin: true
}
]
},
}
]
];
@ -791,6 +808,7 @@ export default function NavDrawer() {
<Route path="discover/endpoint" element={<DiscoverTabs />} />
<Route path="analyze/visualize" element={<AnalyzeTabs />} />
<Route path="analyze/peering" element={<AnalyzeTabs />} />
<Route path="tools/planner" element={<ToolsTabs />} />
<Route path="configure" element={<ConfigureIPAM />} />
{/* <Route path="admin" element={<Administration />} /> */}
<Route path="admin/admins" element={<AdminTabs />} />

Просмотреть файл

@ -0,0 +1,61 @@
import * as React from 'react';
import { Link, useLocation } from "react-router-dom";
import PropTypes from 'prop-types';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
import Planner from '../tools/planner';
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ height: 'calc(100vh - 112px)' }}>
{children}
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
export default function ToolsTabs() {
const allTabs = ['/tools/planner'];
let location = useLocation();
return (
<Box sx={{ width: '100%', height: 'calc(100vh - 137px)'}}>
<React.Fragment>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={allTabs.indexOf(location.pathname)}>
<Tab label="Planner" component={Link} to={allTabs[0]} {...a11yProps(0)} />
</Tabs>
</Box>
<TabPanel value={allTabs.indexOf(location.pathname)} index={0}><Planner /></TabPanel>
</React.Fragment>
</Box>
);
}

Просмотреть файл

@ -0,0 +1,415 @@
import * as React from 'react';
import { useSelector } from 'react-redux';
import { styled } from '@mui/material/styles';
import { useMsal } from "@azure/msal-react";
import { InteractionRequiredAuthError } from "@azure/msal-browser";
import { useSnackbar } from 'notistack';
import { find } from 'lodash';
import {
Box,
Paper,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
Tooltip,
Autocomplete,
ToggleButton,
ToggleButtonGroup,
Typography,
CircularProgress
} from '@mui/material';
import Grid from '@mui/material/Unstable_Grid2';
import {
FilterList as FilterListIcon,
FilterListOff as FilterListOffIcon
} from '@mui/icons-material';
import {
selectVNets
} from "../ipam/ipamSlice";
import {
fetchSubscriptions
} from "../ipam/ipamAPI";
import { apiRequest } from "../../msal/authConfig";
import { availableSubnets } from './utils/iputils';
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: "red",
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
fontSize: "clamp(12px, 1vw, 20px)"
}));
const cidrMasks = [
{ name: '/8', value: 8},
{ name: '/9', value: 9},
{ name: '/10', value: 10},
{ name: '/11', value: 11},
{ name: '/12', value: 12},
{ name: '/13', value: 13},
{ name: '/14', value: 14},
{ name: '/15', value: 15},
{ name: '/16', value: 16},
{ name: '/17', value: 17},
{ name: '/18', value: 18},
{ name: '/19', value: 19},
{ name: '/20', value: 20},
{ name: '/21', value: 21},
{ name: '/22', value: 22},
{ name: '/23', value: 23},
{ name: '/24', value: 24},
{ name: '/25', value: 25},
{ name: '/26', value: 26},
{ name: '/27', value: 27},
{ name: '/28', value: 28},
{ name: '/29', value: 29},
{ name: '/30', value: 30},
{ name: '/31', value: 31},
{ name: '/32', value: 32}
];
const Separator = (props) => {
return (
<Box sx={{ display: "flex", flexDirection: "row", pt: 2, pb: 2 }}>
<div style={{ flexGrow: 0.1 }}>
<hr />
</div>
<div style={{ display: 'flex', alignItems: 'center', margin: '0px 8px' }}>
<Typography>
MASK: {props.name} | ({props.used}/{props.total} Used)
</Typography>
</div>
<div style={{ flexGrow: 2 }}>
<hr />
</div>
</Box>
);
};
const Planner = () => {
const { instance, accounts } = useMsal();
const { enqueueSnackbar } = useSnackbar();
const [subscriptions, setSubscriptions] = React.useState(null);
const [newVNets, setNewVNets] = React.useState([]);
const [subnetData, setSubnetData] = React.useState(null);
const [vNetInput, setVNetInput] = React.useState('');
const [maskInput, setMaskInput] = React.useState('');
const [selectedVNet, setSelectedVNet] = React.useState(null);
const [selectedPrefix, setSelectedPrefix] = React.useState('');
const [selectedMask, setSelectedMask] = React.useState(null);
const [exclusions, setExclusions] = React.useState([]);
const [vNetOptions, setVNetOptions] = React.useState([]);
const [prefixOptions, setPrefixOptions] = React.useState(null);
const [maskOptions, setMaskOptions] = React.useState([]);
const [showAll, setShowAll] = React.useState(false);
const subsLoadingRef = React.useRef(false);
const vNets = useSelector(selectVNets);
const loading = !vNets || subsLoadingRef.current;
const refreshSubscriptions = React.useCallback(() => {
const request = {
scopes: apiRequest.scopes,
account: accounts[0],
};
(async () => {
try {
subsLoadingRef.current = true;
const response = await instance.acquireTokenSilent(request);
const data = await fetchSubscriptions(response.accessToken);
setSubscriptions(data);
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
instance.acquireTokenRedirect(request);
} else {
console.log("ERROR");
console.log("------------------");
console.log(e);
console.log("------------------");
enqueueSnackbar("Error fetching subnets", { variant: "error" });
}
} finally {
subsLoadingRef.current = false;
}
})();
}, [accounts, enqueueSnackbar, instance]);
React.useEffect(() => {
!subsLoadingRef.current && refreshSubscriptions();
}, [vNets, refreshSubscriptions]);
React.useEffect(() => {
if (vNets && subscriptions) {
const subMap = subscriptions.reduce((prev, curr) => {
return {
...prev,
[curr.subscription_id]: curr.name,
};
}, {});
const newNets = vNets.map((vnet) => {
var subName = subMap[vnet.subscription_id] || vnet.subscription_id;
return {
...vnet,
subscription_name: subName,
};
});
setNewVNets(newNets);
}
}, [vNets, subscriptions]);
React.useEffect(() => {
if(!find(newVNets, selectedVNet)) {
setSelectedVNet(null);
setVNetInput('');
}
}, [newVNets, selectedVNet]);
React.useEffect(() => {
setSelectedVNet(null);
setVNetInput('');
}, [showAll]);
React.useEffect(() => {
showAll
? setVNetOptions(newVNets.sort((a, b) => (a.subscription_name > b.subscription_name) ? 1 : -1))
: setVNetOptions(newVNets.filter(v => v.parentSpace !== null).sort((a, b) => (a.parentSpace > b.parentSpace) ? 1 : (a.parentSpace === b.parentSpace) ? ((a.parentBlock > b.parentBlock) ? 1 : -1) : -1 ));
}, [showAll, newVNets]);
React.useEffect(() => {
if (selectedVNet) {
let exclusions = selectedVNet.subnets.map((sub) => sub.prefix);
let prefixParts = selectedVNet.prefixes[0].split("/");
let currentMask = parseInt(prefixParts[1], 10);
let availableMasks = cidrMasks.filter((opt) => opt.value > currentMask && opt.value <= currentMask + 10);
setExclusions(exclusions);
setPrefixOptions(selectedVNet.prefixes);
setSelectedPrefix(selectedVNet.prefixes[0]);
setMaskOptions(availableMasks);
setSelectedMask(availableMasks[0]);
} else {
setExclusions([]);
setPrefixOptions(null);
setSelectedPrefix("");
setSelectedMask(null);
setMaskInput("");
setMaskOptions([]);
}
}, [selectedVNet]);
React.useEffect(() => {
if (selectedPrefix) {
let prefixParts = selectedPrefix.split("/");
let currentMask = parseInt(prefixParts[1], 10);
let availableMasks = cidrMasks.filter((opt) => opt.value > currentMask && opt.value <= currentMask + 10);
setMaskOptions(availableMasks);
setSelectedMask(availableMasks[0]);
} else {
setSelectedMask(null);
setMaskInput("");
setMaskOptions([]);
}
}, [selectedPrefix]);
React.useEffect(() => {
if (selectedVNet && selectedPrefix && selectedMask) {
let prefixParts = selectedPrefix.split("/");
let currentMask = parseInt(prefixParts[1], 10);
let query = {
address: prefixParts[0],
netmask: currentMask,
netmaskRange: { max: selectedMask.value, min: currentMask || 16 },
};
let subnetsObj = availableSubnets(query, exclusions);
setSubnetData(subnetsObj);
} else {
setSubnetData(null);
}
}, [selectedVNet, selectedPrefix, selectedMask, exclusions]);
const handleShowAll = (event, newFilter) => {
if (newFilter !== null) {
setShowAll(newFilter);
}
};
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%'}}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: '8px', pt: 1, pb: 1, pr: 3, pl: 3, alignItems: 'center' }}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: '8px', pt: 1, pb: 1 }}>
<Autocomplete
freeSolo
id="grouped-demo"
size="small"
options={vNetOptions}
groupBy={(option) => showAll ? option.subscription_name : `${option.parentSpace}${option.parentBlock}`}
getOptionLabel={(option) => option.name}
inputValue={vNetInput}
onInputChange={(event, newInputValue) => setVNetInput(newInputValue)}
value={selectedVNet}
onChange={(event, newValue) => setSelectedVNet(newValue)}
sx={{ width: 300 }}
renderInput={(params) => (
<TextField
{...params}
label="Virtual Network"
placeholder={showAll ? "By Subscription" : "By Space ➜ Block"}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
renderGroup={(params) => (
<li key={params.group}>
<Box sx={{ top: '-8px', padding: '4px 10px' }}>{params.group}</Box>
<ul style={{ padding: 0 }}>{params.children}</ul>
</li>
)}
renderOption={(props, option) => {
return (
<li {...props} key={option.id}>
{option.name}
</li>
);
}}
/>
<FormControl size="small">
<InputLabel
disabled={selectedVNet === null}
id="prefix-select-label"
>
Address Space
</InputLabel>
<Select
disabled={selectedVNet === null}
labelId="prefix-select-label"
id="vnet-select"
value={selectedPrefix}
label="Address Space"
onChange={(event) => setSelectedPrefix(event.target.value)}
sx={{ width: '22ch' }}
MenuProps={{
PaperProps: {
style: {
maxHeight: 36 * 10,
}
},
}}
>
{prefixOptions ?
prefixOptions.map((opt) => (
<MenuItem
key={opt}
value={opt}
>
{opt}
</MenuItem>
)) : null
}
</Select>
</FormControl>
<Autocomplete
freeSolo
disabled={selectedPrefix === ''}
id="cidr-mask-max"
size="small"
options={maskOptions}
getOptionLabel={(option) => option.name}
inputValue={maskInput}
onInputChange={(event, newInputValue) => setMaskInput(newInputValue)}
value={selectedMask}
onChange={(event, newValue) => setSelectedMask(newValue)}
sx={{ width: '5ch' }}
renderInput={(params) => <TextField {...params} label="Mask" placeholder="Max" />}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: '8px', pt: 1, mb: 1, marginLeft: 'auto' }}>
<ToggleButtonGroup
size="small"
color="primary"
value={showAll}
exclusive
onChange={handleShowAll}
>
<ToggleButton value={false} aria-label="list">
<Tooltip title="Filter Networks">
<FilterListIcon />
</Tooltip>
</ToggleButton>
<ToggleButton value={true} aria-label="module">
<Tooltip title="All Networks">
<FilterListOffIcon />
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
</Box>
</Box>
<Box sx={{ flexGrow: 1, pb: 3, pr: 3, pl: 3, overflowY: 'scroll', overflowX: 'hidden' }}>
{
subnetData &&
[...new Set(subnetData.subnets.map((x) => x.mask))].map((mask) => {
return (
<React.Fragment key={`fragment-${mask}`}>
<Separator key={`sep-${mask}`} name={mask} total={subnetData.subnets.filter((x) => x.mask === mask).length} used={subnetData.subnets.filter((x) => x.mask === mask && x.overlap).length} />
<Grid key={`grid-container-${mask}`} container spacing={2}>
{
subnetData?.subnets.filter((x) => x.mask === mask).map((item) => {
return (
<Grid key={`grid-item-${item.network}-${mask}`} xs={5} sm={3} md={2}>
<Item
style={{
backgroundColor: item.overlap ? "orangered" : "lawngreen",
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{item.network}/{mask}
</Item>
</Grid>
);
})
}
</Grid>
</React.Fragment>
);
})}
</Box>
</Box>
);
}
export default Planner;

Просмотреть файл

@ -0,0 +1,166 @@
function allowedOctets() {
return [128, 64, 32, 16, 8, 4, 2, 1, 128, 64, 32, 16];
}
function ip2Integer(ip) {
return ip.split('.').reduce(function(ipInt, octet) { return (ipInt << 8) + parseInt(octet, 10) }, 0) >>> 0;
}
function probabalCombinations(arr, Addressbytes, position) {
var res = [];
for (var i = 0; i < Math.pow(2, arr.length); i++) {
var bin = (i).toString(2);
var set = [];
bin = new Array((arr.length - bin.length) + 1).join("0") + bin;
for (var j = 0; j < bin.length; j++) {
if (bin[j] === "1") {
set.push(arr[j]);
}
}
try {
var sum = set.reduce((a, b) => { return a + b; });
res.push(sum);
} catch(e) {
continue;
}
}
res = res.filter(n => { return n >= Addressbytes[position] });
arr.indexOf(0) !== -1 ?? res.push(0);
res = [...new Set(res)];
return res;
}
function getIpRangeForSubnet(subnetCIDR) {
var address = subnetCIDR.split('/')[0].split('.');
var netmask = parseInt(subnetCIDR.split('/')[1], 10);
var allowed = allowedOctets();
var pos = Math.ceil(netmask / 8) - 1;
var endAddress = [...address];
endAddress[pos] = parseInt(endAddress[pos], 10) + ((!allowed[(netmask % 8) - 1]) ? 0 : allowed[(netmask % 8) - 1] - 1);
if(pos === 2 && endAddress[3] < 255) {
endAddress[3] = 255;
}
return {'start': address.join('.'), 'end': endAddress.join('.')};
}
function isSubnetOverlap(subnetCIDR, existingSubnetCIDR) {
var ipRangeforCurrent = getIpRangeForSubnet(subnetCIDR);
var isOverlap = existingSubnetCIDR.map(subnet => {
var ipRange = getIpRangeForSubnet(subnet);
if((ip2Integer(ipRangeforCurrent.start) >= ip2Integer(ipRange.start) && ip2Integer(ipRangeforCurrent.start) <= ip2Integer(ipRange.end)) || (ip2Integer(ipRangeforCurrent.end) >= ip2Integer(ipRange.start) && ip2Integer(ipRangeforCurrent.end) <= ip2Integer(ipRange.end)) || (ip2Integer(ipRange.start) >= ip2Integer(ipRangeforCurrent.start) && ip2Integer(ipRange.start) <= ip2Integer(ipRangeforCurrent.end)) || (ip2Integer(ipRange.end) >= ip2Integer(ipRangeforCurrent.start) && ip2Integer(ipRange.end) <= ip2Integer(ipRangeforCurrent.end)) ) {
return true;
}
return false;
}).some(item => item === true);
return isOverlap;
}
function possibleSubnets(obj, index, existingSubnetCIDR) {
var sliceTo = ((index % 8) === 0) ? 8 : (index % 8);
var filteredOctets = [];
var pos = Math.ceil(index / 8) - 1;
var subnets = [];
var subnetsExcluded = [];
var allowed = allowedOctets();
var addressBytes = obj.address.split('.', 4).map(num => parseInt(num, 10));
if((obj.netmask === 24 && index === 24) || (obj.netmask === 16 && index === 16)) {
filteredOctets.push(addressBytes[2]);
} else if((obj.netmask % 8) <= sliceTo && index <= 24) {
filteredOctets = allowed.slice(obj.netmask%8, sliceTo);
filteredOctets.push(addressBytes[2]);
} else if(index >= 24 && addressBytes[3] === 0) {
filteredOctets = allowed.slice(0, sliceTo);
filteredOctets.push(addressBytes[3]);
} else {
filteredOctets = allowed.slice(0, sliceTo);
}
var allowedCombinations = probabalCombinations(filteredOctets, addressBytes, pos);
allowedCombinations.forEach(function(octet) {
let range = (index >= 25 & obj.netmask < 24) ? {
'from': addressBytes[2],
'to': addressBytes[2] + ((allowed[(obj.netmask % 8) - 1] === undefined) ? 256 : allowed[(obj.netmask % 8) - 1]) - 1
} : {
'from': addressBytes[2],
'to': addressBytes[2]
}
for (let i = range.from; i <= range.to; i++) {
var subnetBytes = [...addressBytes];
subnetBytes[2] = i;
subnetBytes[pos] = octet;
var subnetObject = {
'network': subnetBytes.join('.'),
'mask': index,
'cidr': subnetBytes.join('.') + '/' + index,
'ipRange': getIpRangeForSubnet(subnetBytes.join('.') + '/' + index)
};
var doesOverlap = isSubnetOverlap(subnetObject.cidr, existingSubnetCIDR);
if(!doesOverlap) {
subnetObject.overlap = false;
subnets.push(subnetObject);
} else {
subnetObject.overlap = true;
subnets.push(subnetObject);
subnetsExcluded.push(subnetObject.cidr);
}
}
});
return {'subnets': subnets, 'subnetsExcluded': subnetsExcluded};
}
export function availableSubnets(obj, existingSubnetCIDR) {
var subnetsObj = {
subnets: [],
subnetsExcluded: []
};
var startIndex = obj.netmaskRange.min;
for(var i = startIndex; i <= obj.netmaskRange.max; i++) {
var res = possibleSubnets(obj, i, existingSubnetCIDR);
subnetsObj.subnets = [...subnetsObj.subnets, ...res.subnets];
subnetsObj.subnetsExcluded = [...subnetsObj.subnetsExcluded, ...res.subnetsExcluded];
}
subnetsObj.subnets = subnetsObj.subnets.sort((a,b) => {
let netA = ip2Integer(a.network);
let netB = ip2Integer(b.network);
if (a.mask < b.mask)
return -1
else if (a.mask > b.mask)
return 1
else if (a.mask === b.mask)
if (netA > netB)
return 1
else
return -1
return 0;
});
return subnetsObj;
}

21
ui/src/img/Planner.js Normal file
Просмотреть файл

@ -0,0 +1,21 @@
import React from "react";
function Planner() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0H24V24H0z"></path>
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14z"></path>
<path d="M7 12H9V17H7z"></path>
<path d="M15 7H17V17H15z"></path>
<path d="M11 14H13V17H11z"></path>
<path d="M11 10H13V12H11z"></path>
</svg>
);
}
export default Planner;

17
ui/src/img/Tools.js Normal file
Просмотреть файл

@ -0,0 +1,17 @@
import React from "react";
function Tools() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0H24V24H0z"></path>
<path d="M20 8h-3V6c0-1.1-.9-2-2-2H9c-1.1 0-2 .9-2 2v2H4c-1.1 0-2 .9-2 2v10h20V10c0-1.1-.9-2-2-2zM9 6h6v2H9V6zm11 12H4v-3h2v1h2v-1h8v1h2v-1h2v3zm-2-5v-1h-2v1H8v-1H6v1H4v-3h16v3h-2z"></path>
</svg>
);
}
export default Tools;

1
ui/src/img/planner.svg Normal file
Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/><g><path d="M19,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.1,3,19,3z M19,19H5V5h14V19z"/><rect height="5" width="2" x="7" y="12"/><rect height="10" width="2" x="15" y="7"/><rect height="3" width="2" x="11" y="14"/><rect height="2" width="2" x="11" y="10"/></g></g></svg>

После

Ширина:  |  Высота:  |  Размер: 459 B

1
ui/src/img/tools.svg Normal file
Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M20,8h-3V6c0-1.1-0.9-2-2-2H9C7.9,4,7,4.9,7,6v2H4c-1.1,0-2,0.9-2,2v10h20V10C22,8.9,21.1,8,20,8z M9,6h6v2H9V6z M20,18H4 v-3h2v1h2v-1h8v1h2v-1h2V18z M18,13v-1h-2v1H8v-1H6v1H4v-3h3h10h3v3H18z"/></g></g></svg>

После

Ширина:  |  Высота:  |  Размер: 385 B