Add initial implementation of the VPC command

This commit is contained in:
Borys_Levytskyi
2021-04-23 20:38:23 +03:00
parent 32be8a87c9
commit 99dba2ba2b
11 changed files with 232 additions and 29 deletions

View File

@@ -12,7 +12,7 @@ export type FlipBitEventArg = {
index: number;
binaryString: string;
$event: any;
newBinaryString: string,
newBinaryString: string
};
export default class BinaryStringView extends React.Component<BinaryStringViewProps> {

View File

@@ -33,6 +33,8 @@ function getBase(kind:string) : number {
throw new Error("Unsupported kind: " + kind);
}
export default formatter;
const emBin = formatter.emBin;
export {emBin};
const emBin = formatter.emBin.bind(formatter);
const padLeft = formatter.padLeft.bind(formatter);
export {emBin, padLeft}
export default formatter;

View File

@@ -107,6 +107,7 @@ code { font-size: 1.2em; font-weight: bold; }
.midnight .zero { color: #85a0ad;}
.midnight .prefix { color: #85a0ad}
.midnight .other { color: #9FBAC7;}
.midnight .accent-background { background-color: #3b5268;}
.midnight .hashLink, .dark .hashLink:visited { color: #85a0ad }
.midnight .hashLink:hover { color: #9FBAC7 }
.midnight ul.top-links li:hover { background: #132537 }

View File

@@ -32,7 +32,7 @@ function SubnetView(props : {subnet : SubnetCommand}) {
<span>Network Size</span>
</td>
<td data-test-name="decimal">
{subnet.getAdressSpaceSize()}
{subnet.input.getAdressSpaceSize()}
</td>
</tr>
<tr>

View File

@@ -0,0 +1,23 @@
.vpc-view .description {
vertical-align: middle;
text-align: right;
}
.vpc-view td {
padding-right: 15px;
}
.vpc-view {
margin-bottom: 20px;
}
.vpc-view .part {
border-bottom: solid 1px;
}
.vpc-view button {
margin:0 3px;
}
.vpc-view .ip-address-col { min-width: 8.5em;}

View File

@@ -0,0 +1,115 @@
import React, { useState } from 'react';
import BinaryStringView from '../../core/components/BinaryString';
import './SubnetView.css';
import { getNetworkAddress, getBroadCastAddress, createSubnetMaskIp, getAddressSpaceSize } from '../subnet-utils';
import { chunkifyString } from '../../core/utils';
import IpAddressBinaryString from './IpAddressBinaryString';
import { IpAddress, IpAddressWithSubnetMask, SubnetCommand, VpcCommand } from '../models';
import { padLeft } from '../../core/formatter';
function SubnetView(props : {vpc : VpcCommand}) {
const [vpc, setVpc] = useState(VpcModel.create(props.vpc));
const subnetMask = vpc.cidr.maskBits + vpc.subnetBits;
const vpcPart = getNetworkAddress(vpc.cidr).toBinaryString(true).substring(0, vpc.cidr.maskBits);
const subnetsPart = padLeft("0", vpc.subnetBits, "0");
const lastPart = padLeft("0", 32 - (vpc.cidr.maskBits + vpc.subnetBits), "0");
const decrSubnet = () => setVpc(vpc.changeSubnetBits(vpc.subnetBits-1));
const incrSubnet = () => setVpc(vpc.changeSubnetBits(vpc.subnetBits+1));
const incrVpc = () => setVpc(vpc.changeVpcCidr(new IpAddressWithSubnetMask(vpc.cidr.ipAddress, vpc.cidr.maskBits+1)));
const decrVpc = () => setVpc(vpc.changeVpcCidr(new IpAddressWithSubnetMask(vpc.cidr.ipAddress, vpc.cidr.maskBits-1)));
return <React.Fragment>
<div className="expression vpc-view">
<table>
<tr>
<td>
VPC Address Space
</td>
<td>
<BinaryStringView binaryString={vpcPart} /><BinaryStringView binaryString={subnetsPart} className="accent-background" /><BinaryStringView binaryString={lastPart} />
</td>
</tr>
<tr>
<td>
VPC Mask:
</td>
<td>
<button onClick={decrVpc} disabled={vpc.cidr.maskBits <= 1} title="Increase vpc space">-</button>
/{vpc.cidr.maskBits}
<button onClick={incrVpc} disabled={subnetMask >= 31} title="Decrease vpc space">+</button>
</td>
</tr>
<tr>
<td>
Subnet Mask:
</td>
<td>
<button onClick={decrSubnet} disabled={vpc.subnetBits <= 1} title="Increase subnet space">-</button>
/{subnetMask}
<button onClick={incrSubnet} disabled={vpc.cidr.maskBits + vpc.subnetBits >= 31} title="Decrease subnet space">+</button>
</td>
</tr>
<tr>
<td>
Max Subnets:
</td>
<td>
{Math.pow(2, vpc.subnetBits)}
</td>
</tr>
<tr>
<td>
Subnet Network Size:
</td>
<td>
{getAddressSpaceSize(subnetMask)}
</td>
</tr>
</table>
</div>
</React.Fragment>;
}
function VpcRow(props: { ip: IpAddress, descr: string}) {
const {ip, descr} = props;
return <tr>
<td className="soft" data-test-name="label">{descr}</td>
<td data-test-name="decimal" className="ip-address-col">
{ip.toString()}
</td>
<td data-test-name="bin">
<IpAddressBinaryString ip={ip} />
</td>
</tr>;
}
export default SubnetView;
class VpcModel {
cidr: IpAddressWithSubnetMask;
subnetBits: number;
subnetNum: number;
constructor(cidr: IpAddressWithSubnetMask, subnetBits: number) {
this.cidr = cidr;
this.subnetBits = subnetBits;
this.subnetNum = 0;
}
static create(vpc: VpcCommand) {
return new VpcModel(vpc.cidr, vpc.subnetBits);
}
changeSubnetBits(n: number) {
return new VpcModel(this.cidr, n);
}
changeVpcCidr(newCidr: IpAddressWithSubnetMask) {
return new VpcModel(newCidr, this.subnetBits);
}
}

View File

@@ -1,5 +1,5 @@
import ipAddressParser, { ParsedIpObject, ParsingError } from './ip-parser';
import { IpAddressWithSubnetMask, IpAddress, SubnetCommand } from "./models";
import { IpAddressWithSubnetMask, IpAddress, SubnetCommand, VpcCommand } from "./models";
describe('parser tests', () => {
@@ -62,4 +62,11 @@ describe('parser tests', () => {
const subnet = actual as SubnetCommand;
expect(subnet.toString()).toBe('192.168.1.1/23');
});
it('parses vpc command', () => {
const actual = ipAddressParser.parse('vpc 192.168.1.1/23');
expect(actual).toBeInstanceOf(VpcCommand);
const vpc = actual as VpcCommand;
expect(vpc.toString()).toBe('192.168.1.1/23');
});
});

View File

@@ -1,10 +1,18 @@
import { IpAddress, IpAddressWithSubnetMask, SubnetCommand } from './models';
import { IpAddress, IpAddressWithSubnetMask, SubnetCommand, VpcCommand } from './models';
export type ParsedIpObject = IpAddress | IpAddressWithSubnetMask;
export type ParsingResult = ParsedIpObject[] |
SubnetCommand |
ParsingError |
VpcCommand |
null;
const CMD_SUBNET = 'subnet';
const CMD_VPC = 'vpc';
const ipAddressParser = {
parse: function(input: string) : ParsedIpObject[] | SubnetCommand | ParsingError | null {
parse: function(input: string) : ParsingResult {
const result = this.parseCommand(input);
@@ -27,12 +35,12 @@ const ipAddressParser = {
}
if(result.command != null) {
const result = this.createSubnetDefinition(parsedObjects as ParsedIpObject[]);
const cmd =
result.command == CMD_SUBNET
? this.createSubnetDefinition(parsedObjects as ParsedIpObject[])
: this.createVpcDefinition(parsedObjects as ParsedIpObject[]);
if(result instanceof ParsingError)
return result;
return result;
return cmd;
}
return parsedObjects as ParsedIpObject[];
@@ -40,9 +48,14 @@ const ipAddressParser = {
parseCommand(input : string) : { command: null | string, nextInput: string } {
const command = 'subnet';
if(input.startsWith(command))
return { command, nextInput: input.substring(command.length)}
if(input.startsWith(CMD_SUBNET))
return { command: CMD_SUBNET, nextInput: input.substring(CMD_SUBNET.length)};
if(input.startsWith(CMD_VPC)) {
return {command: CMD_VPC, nextInput: input.substring(CMD_VPC.length)};
}
return { command: null, nextInput: input };
},
@@ -101,6 +114,19 @@ const ipAddressParser = {
}
return new ParsingError("Network definition requires subnet mask");
},
createVpcDefinition(items: ParsedIpObject[]) : VpcCommand | ParsingError {
if(items.length != 1)
return new ParsingError("Incorrect VPC definition");
const first = items[0];
if(first instanceof IpAddressWithSubnetMask) {
return new VpcCommand(first);
}
return new ParsingError("VPC definition requires subnet mask");
}
}
@@ -111,5 +137,4 @@ export class ParsingError {
}
}
export default ipAddressParser;

View File

@@ -1,4 +1,6 @@
import {emBin} from "../core/formatter";
import { numberParser } from "../expression/numberParser";
import { getAddressSpaceSize } from "./subnet-utils";
export type OctetNumber = 1 | 2 | 3 | 4;
export type NetworkClass = 'a' | 'b' | 'c' | 'd' | 'e';
@@ -12,6 +14,11 @@ export class IpAddressWithSubnetMask {
this.maskBits = maskBits;
}
getAdressSpaceSize(): number {
const spaceLengthInBits = 32 - this.maskBits;
return getAddressSpaceSize(spaceLengthInBits);
}
toString() {
return `${this.ipAddress.toString()}/${this.maskBits}`;
}
@@ -35,9 +42,12 @@ export class IpAddress {
return `${this.firstByte}.${this.secondByte}.${this.thirdByte}.${this.fourthByte}`;
}
toBinaryString() {
toBinaryString(skipDots?: boolean) {
return `${emBin(this.firstByte)}).${emBin(this.secondByte)}.${emBin(this.thirdByte)}.${emBin(this.fourthByte)}`;
if(!skipDots)
return `${emBin(this.firstByte)}.${emBin(this.secondByte)}.${emBin(this.thirdByte)}.${emBin(this.fourthByte)}`;
else
return `${emBin(this.firstByte)}${emBin(this.secondByte)}${emBin(this.thirdByte)}${emBin(this.fourthByte)}`;
}
clone(): IpAddress {
@@ -63,18 +73,25 @@ export class IpAddress {
}
export class SubnetCommand {
input: IpAddressWithSubnetMask;
input: IpAddressWithSubnetMask; // TODO: rename to cidr
constructor(definition: IpAddressWithSubnetMask) {
this.input = definition;
}
getAdressSpaceSize(): number {
const spaceLengthInBits = 32 - this.input.maskBits;
return Math.pow(2, spaceLengthInBits) - 2; // 0 - network address, 1 - multicast address
}
toString() {
return this.input.toString();
}
}
export class VpcCommand {
cidr: IpAddressWithSubnetMask;
subnetBits: number;
constructor(cidr: IpAddressWithSubnetMask) {
this.cidr = cidr;
this.subnetBits = 3;
}
toString() {
return this.cidr.toString();
}
}

View File

@@ -4,11 +4,13 @@ import { CmdShell, CommandInput, CommandOptions } from '../shell/cmd';
import ErrorResultView from '../shell/components/ErrorResultView';
import IpAddressView from './components/IpAddressView';
import ipAddressParser, {ParsingError, ParsedIpObject} from './ip-parser';
import { IpAddress, IpAddressWithSubnetMask, SubnetCommand } from "./models";
import { IpAddress, IpAddressWithSubnetMask, SubnetCommand, VpcCommand } from "./models";
import log from 'loglevel';
import SubnetView from './components/SubnetView';
import { createSubnetMaskIp } from './subnet-utils';
import {sendAnalyticsEvent} from '../shell/analytics';
import TextResultView from '../shell/components/TextResultView';
import VpcView from './components/VpcView';
const networkingAppModule = {
setup: function(appState: AppState, cmd: CmdShell) {
@@ -33,6 +35,12 @@ const networkingAppModule = {
return;
}
if(result instanceof VpcCommand) {
appState.addCommandResult(c.input, <VpcView vpc={result} />);
trackCommand('VpcCommand', c.options);
return;
}
const ipAddresses : IpAddress[] = [];
(result as ParsedIpObject[]).forEach(r => {
@@ -43,7 +51,7 @@ const networkingAppModule = {
}
else if(r instanceof IpAddress) {
ipAddresses.push(r);
}
}
});
trackCommand("IpAddressesInput", c.options);

View File

@@ -29,6 +29,11 @@ function getBroadCastAddress(ipm: IpAddressWithSubnetMask) : IpAddress {
return flipSubnetMaskBits(ipm, flipBitsToOne, 255);
}
function getAddressSpaceSize(maskSize: number) {
const spaceLengthInBits = 32 - maskSize;
return Math.pow(2, spaceLengthInBits) - 2; // 0 - network address, 1 - multicast address
}
function flipSubnetMaskBits(ipm: IpAddressWithSubnetMask, flipper : FlipFunction, fullByte: number) {
// Cannot treat ip address as a single number operation because 244 << 24 results in a negative number in JS
const flip = (maskBits: number, byte: number) => flipper(byte, 8 - maskBits);
@@ -80,4 +85,4 @@ function getNetworkClass (ipAddress: IpAddress) : NetworkClass {
type FlipFunction = (byte: number, numberOfBits: number) => number;
export {createSubnetMaskIp, getBroadCastAddress, getNetworkAddress, getNetworkClass};
export {createSubnetMaskIp, getBroadCastAddress, getNetworkAddress, getNetworkClass, getAddressSpaceSize};