first commit
This commit is contained in:
commit
8b341a9f3b
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Frontend
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
my-recipes-chart/\
|
||||||
|
helm/
|
||||||
121
.woodpecker.yaml
Normal file
121
.woodpecker.yaml
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
steps:
|
||||||
|
build-frontend:
|
||||||
|
name: Build & Push Frontend
|
||||||
|
image: woodpeckerci/plugin-kaniko
|
||||||
|
when:
|
||||||
|
branch: [ master, develop ]
|
||||||
|
event: [ push, pull_request, tag ]
|
||||||
|
path:
|
||||||
|
include: [ frontend/** ]
|
||||||
|
settings:
|
||||||
|
registry: harbor.dvirlabs.com
|
||||||
|
repo: my-apps/${CI_REPO_NAME}-frontend
|
||||||
|
dockerfile: frontend/Dockerfile
|
||||||
|
context: frontend
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
|
||||||
|
username:
|
||||||
|
from_secret: DOCKER_USERNAME
|
||||||
|
password:
|
||||||
|
from_secret: DOCKER_PASSWORD
|
||||||
|
|
||||||
|
build-backend:
|
||||||
|
name: Build & Push Backend
|
||||||
|
image: woodpeckerci/plugin-kaniko
|
||||||
|
when:
|
||||||
|
branch: [ master, develop ]
|
||||||
|
event: [ push, pull_request, tag ]
|
||||||
|
path:
|
||||||
|
include: [ backend/** ]
|
||||||
|
settings:
|
||||||
|
registry: harbor-core.dev-tools.svc.cluster.local
|
||||||
|
repo: my-apps/${CI_REPO_NAME}-backend
|
||||||
|
dockerfile: backend/Dockerfile
|
||||||
|
context: backend
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
|
||||||
|
username:
|
||||||
|
from_secret: DOCKER_USERNAME
|
||||||
|
password:
|
||||||
|
from_secret: DOCKER_PASSWORD
|
||||||
|
|
||||||
|
update-values-frontend:
|
||||||
|
name: Update frontend tag in values.yaml
|
||||||
|
image: alpine:3.19
|
||||||
|
when:
|
||||||
|
branch: [ master, develop ]
|
||||||
|
event: [ push ]
|
||||||
|
path:
|
||||||
|
include: [ frontend/** ]
|
||||||
|
environment:
|
||||||
|
GIT_USERNAME:
|
||||||
|
from_secret: GIT_USERNAME
|
||||||
|
GIT_TOKEN:
|
||||||
|
from_secret: GIT_TOKEN
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache git yq
|
||||||
|
- git config --global user.name "woodpecker-bot"
|
||||||
|
- git config --global user.email "ci@dvirlabs.com"
|
||||||
|
- git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git"
|
||||||
|
- cd my-apps
|
||||||
|
- |
|
||||||
|
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
||||||
|
echo "💡 Setting frontend tag to: $TAG"
|
||||||
|
yq -i ".frontend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
|
||||||
|
git add manifests/${CI_REPO_NAME}/values.yaml
|
||||||
|
git commit -m "frontend: update tag to $TAG" || echo "No changes"
|
||||||
|
git push origin HEAD
|
||||||
|
|
||||||
|
update-values-backend:
|
||||||
|
name: Update backend tag in values.yaml
|
||||||
|
image: alpine:3.19
|
||||||
|
when:
|
||||||
|
branch: [ master, develop ]
|
||||||
|
event: [ push ]
|
||||||
|
path:
|
||||||
|
include: [ backend/** ]
|
||||||
|
environment:
|
||||||
|
GIT_USERNAME:
|
||||||
|
from_secret: GIT_USERNAME
|
||||||
|
GIT_TOKEN:
|
||||||
|
from_secret: GIT_TOKEN
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache git yq
|
||||||
|
- git config --global user.name "woodpecker-bot"
|
||||||
|
- git config --global user.email "ci@dvirlabs.com"
|
||||||
|
- |
|
||||||
|
if [ ! -d "my-apps" ]; then
|
||||||
|
git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git"
|
||||||
|
fi
|
||||||
|
- cd my-apps
|
||||||
|
- |
|
||||||
|
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
||||||
|
echo "💡 Setting backend tag to: $TAG"
|
||||||
|
yq -i ".backend.image.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
|
||||||
|
git add manifests/${CI_REPO_NAME}/values.yaml
|
||||||
|
git commit -m "backend: update tag to $TAG" || echo "No changes"
|
||||||
|
git push origin HEAD
|
||||||
|
|
||||||
|
|
||||||
|
trigger-gitops-via-push:
|
||||||
|
when:
|
||||||
|
branch: [ master, develop ]
|
||||||
|
event: [ push ]
|
||||||
|
name: Trigger apps-gitops via Git push
|
||||||
|
image: alpine/git
|
||||||
|
environment:
|
||||||
|
GIT_USERNAME:
|
||||||
|
from_secret: GIT_USERNAME
|
||||||
|
GIT_TOKEN:
|
||||||
|
from_secret: GIT_TOKEN
|
||||||
|
commands: |
|
||||||
|
git config --global user.name "woodpecker-bot"
|
||||||
|
git config --global user.email "ci@dvirlabs.com"
|
||||||
|
git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/apps-gitops.git"
|
||||||
|
cd apps-gitops
|
||||||
|
echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger
|
||||||
|
git add .trigger
|
||||||
|
git commit -m "ci: trigger apps-gitops build" || echo "no changes"
|
||||||
|
git push origin HEAD
|
||||||
154
README.md
Normal file
154
README.md
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
# IP Subnet Calculator
|
||||||
|
|
||||||
|
A full-stack IP subnet calculator application with React + Vite frontend and FastAPI backend.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Calculate Network ID
|
||||||
|
- ✅ Calculate Broadcast Address
|
||||||
|
- ✅ Show First and Last Usable IP addresses
|
||||||
|
- ✅ Display Total Hosts and Usable Hosts
|
||||||
|
- ✅ Show Subnet Mask and Wildcard Mask
|
||||||
|
- ✅ Identify Network Class (A, B, C, D, E)
|
||||||
|
- ✅ Detect IP Type (Unicast, Multicast, Loopback, etc.)
|
||||||
|
- ✅ Check if IP is Private or Public
|
||||||
|
- ✅ Support both CIDR notation and Subnet Mask input
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ip-calculator/
|
||||||
|
├── backend/ # FastAPI backend
|
||||||
|
│ ├── main.py
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── README.md
|
||||||
|
└── frontend/ # React + Vite frontend
|
||||||
|
├── src/
|
||||||
|
│ ├── App.jsx
|
||||||
|
│ ├── App.css
|
||||||
|
│ ├── main.jsx
|
||||||
|
│ └── index.css
|
||||||
|
├── index.html
|
||||||
|
├── package.json
|
||||||
|
└── vite.config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### Backend Setup
|
||||||
|
|
||||||
|
1. Navigate to the backend directory:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create and activate a virtual environment:
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
python -m venv venv
|
||||||
|
venv\Scripts\activate
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the FastAPI server:
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
The API will be available at `http://localhost:8000`
|
||||||
|
API documentation at `http://localhost:8000/docs`
|
||||||
|
|
||||||
|
### Frontend Setup
|
||||||
|
|
||||||
|
1. Navigate to the frontend directory:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:3000`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Start the backend server (port 8000)
|
||||||
|
2. Start the frontend server (port 3000)
|
||||||
|
3. Open your browser and go to `http://localhost:3000`
|
||||||
|
4. Enter an IP address (e.g., 192.168.1.1)
|
||||||
|
5. Choose input method:
|
||||||
|
- **CIDR Notation**: Enter a value between 1-32 (e.g., 24)
|
||||||
|
- **Subnet Mask**: Enter a subnet mask (e.g., 255.255.255.0)
|
||||||
|
6. Click "Calculate Subnet" to see all subnet information
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### POST /calculate
|
||||||
|
Calculate subnet information for a given IP address.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.1.1",
|
||||||
|
"cidr": 24
|
||||||
|
}
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.1.1",
|
||||||
|
"subnet_mask": "255.255.255.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.1.1",
|
||||||
|
"cidr": 24,
|
||||||
|
"subnet_mask": "255.255.255.0",
|
||||||
|
"network_id": "192.168.1.0",
|
||||||
|
"broadcast_address": "192.168.1.255",
|
||||||
|
"first_usable_ip": "192.168.1.1",
|
||||||
|
"last_usable_ip": "192.168.1.254",
|
||||||
|
"total_hosts": 256,
|
||||||
|
"usable_hosts": 254,
|
||||||
|
"wildcard_mask": "0.0.0.255",
|
||||||
|
"network_class": "C",
|
||||||
|
"is_private": true,
|
||||||
|
"ip_type": "Unicast"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **FastAPI**: Modern, fast web framework for building APIs
|
||||||
|
- **Uvicorn**: ASGI server for running FastAPI
|
||||||
|
- **Pydantic**: Data validation using Python type annotations
|
||||||
|
- **ipaddress**: Python standard library for IP address manipulation
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **React**: UI library for building interactive interfaces
|
||||||
|
- **Vite**: Fast build tool and development server
|
||||||
|
- **Axios**: HTTP client for API requests
|
||||||
|
- **CSS3**: Modern styling with gradients and animations
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
16
backend/Dockerfile
Normal file
16
backend/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy requirements and install dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY main.py .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
26
backend/README.md
Normal file
26
backend/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# IP Subnet Calculator - Backend
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Create a virtual environment:
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Activate the virtual environment:
|
||||||
|
- Windows: `venv\Scripts\activate`
|
||||||
|
- Linux/Mac: `source venv/bin/activate`
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
API will be available at http://localhost:8000
|
||||||
|
API documentation at http://localhost:8000/docs
|
||||||
264
backend/main.py
Normal file
264
backend/main.py
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel, validator
|
||||||
|
import ipaddress
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
app = FastAPI(title="IP Subnet Calculator API")
|
||||||
|
|
||||||
|
# CORS configuration
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
class SubnetInput(BaseModel):
|
||||||
|
ip_address: str
|
||||||
|
cidr: Optional[int] = None
|
||||||
|
subnet_mask: Optional[str] = None
|
||||||
|
|
||||||
|
@validator('ip_address')
|
||||||
|
def validate_ip_address(cls, v):
|
||||||
|
if not v or not v.strip():
|
||||||
|
raise ValueError("IP address cannot be empty")
|
||||||
|
|
||||||
|
# Check basic format
|
||||||
|
ip_pattern = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$'
|
||||||
|
if not re.match(ip_pattern, v):
|
||||||
|
raise ValueError("Invalid IP address format. Expected format: xxx.xxx.xxx.xxx")
|
||||||
|
|
||||||
|
# Check each octet is valid (0-255)
|
||||||
|
octets = v.split('.')
|
||||||
|
for i, octet in enumerate(octets):
|
||||||
|
try:
|
||||||
|
num = int(octet)
|
||||||
|
if num < 0 or num > 255:
|
||||||
|
raise ValueError(f"Octet {i+1} must be between 0 and 255. Got: {num}")
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid octet value: {octet}")
|
||||||
|
|
||||||
|
# Validate with ipaddress library
|
||||||
|
try:
|
||||||
|
ipaddress.IPv4Address(v)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Invalid IP address: {str(e)}")
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
@validator('cidr')
|
||||||
|
def validate_cidr(cls, v):
|
||||||
|
if v is not None:
|
||||||
|
if not isinstance(v, int):
|
||||||
|
raise ValueError("CIDR must be a number")
|
||||||
|
if v < 0 or v > 32:
|
||||||
|
raise ValueError("CIDR must be between 0 and 32")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@validator('subnet_mask')
|
||||||
|
def validate_subnet_mask(cls, v):
|
||||||
|
if v is not None:
|
||||||
|
if not v.strip():
|
||||||
|
raise ValueError("Subnet mask cannot be empty")
|
||||||
|
|
||||||
|
# Check format
|
||||||
|
parts = v.split('.')
|
||||||
|
if len(parts) != 4:
|
||||||
|
raise ValueError("Subnet mask must have 4 octets (e.g., 255.255.255.0)")
|
||||||
|
|
||||||
|
# Validate with ipaddress
|
||||||
|
try:
|
||||||
|
mask = ipaddress.IPv4Address(v)
|
||||||
|
# Check if it's a valid subnet mask (contiguous 1s followed by 0s)
|
||||||
|
mask_int = int(mask)
|
||||||
|
# Valid masks have all 1s on the left and all 0s on the right
|
||||||
|
# Check by converting to binary and verifying pattern
|
||||||
|
binary = bin(mask_int)[2:].zfill(32)
|
||||||
|
if '01' in binary: # If there's a 0 followed by 1, it's invalid
|
||||||
|
raise ValueError("Invalid subnet mask: bits must be contiguous (all 1s followed by all 0s)")
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Invalid subnet mask: {str(e)}")
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
class SubnetInfo(BaseModel):
|
||||||
|
ip_address: str
|
||||||
|
ip_address_binary: str
|
||||||
|
cidr: int
|
||||||
|
subnet_mask: str
|
||||||
|
subnet_mask_binary: str
|
||||||
|
network_id: str
|
||||||
|
network_id_binary: str
|
||||||
|
broadcast_address: str
|
||||||
|
broadcast_binary: str
|
||||||
|
first_usable_ip: str
|
||||||
|
last_usable_ip: str
|
||||||
|
total_hosts: int
|
||||||
|
usable_hosts: int
|
||||||
|
wildcard_mask: str
|
||||||
|
wildcard_binary: str
|
||||||
|
network_class: str
|
||||||
|
is_private: bool
|
||||||
|
ip_type: str
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root():
|
||||||
|
return {"message": "IP Subnet Calculator API", "version": "1.0.0"}
|
||||||
|
|
||||||
|
@app.post("/calculate", response_model=SubnetInfo)
|
||||||
|
def calculate_subnet(subnet_input: SubnetInput):
|
||||||
|
try:
|
||||||
|
# Validate that at least one of CIDR or subnet mask is provided
|
||||||
|
if subnet_input.cidr is None and subnet_input.subnet_mask is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="You must provide either CIDR notation (e.g., 24) or subnet mask (e.g., 255.255.255.0)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine CIDR notation
|
||||||
|
if subnet_input.cidr is not None:
|
||||||
|
cidr = subnet_input.cidr
|
||||||
|
ip_with_cidr = f"{subnet_input.ip_address}/{cidr}"
|
||||||
|
elif subnet_input.subnet_mask is not None:
|
||||||
|
# Convert subnet mask to CIDR
|
||||||
|
try:
|
||||||
|
subnet_mask = ipaddress.IPv4Address(subnet_input.subnet_mask)
|
||||||
|
cidr = sum(bin(int(x)).count('1') for x in str(subnet_mask).split('.'))
|
||||||
|
|
||||||
|
# Validate CIDR is reasonable
|
||||||
|
if cidr == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Invalid subnet mask: 0.0.0.0 is not a valid subnet mask"
|
||||||
|
)
|
||||||
|
|
||||||
|
ip_with_cidr = f"{subnet_input.ip_address}/{cidr}"
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid subnet mask format: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate the IP address
|
||||||
|
try:
|
||||||
|
ip_obj = ipaddress.IPv4Address(subnet_input.ip_address)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid IP address format: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create network object
|
||||||
|
try:
|
||||||
|
network = ipaddress.IPv4Network(ip_with_cidr, strict=False)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Cannot create network from IP {subnet_input.ip_address} with CIDR /{cidr}: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate network class
|
||||||
|
first_octet = int(subnet_input.ip_address.split('.')[0])
|
||||||
|
if first_octet < 128:
|
||||||
|
network_class = "A"
|
||||||
|
elif first_octet < 192:
|
||||||
|
network_class = "B"
|
||||||
|
elif first_octet < 224:
|
||||||
|
network_class = "C"
|
||||||
|
elif first_octet < 240:
|
||||||
|
network_class = "D (Multicast)"
|
||||||
|
else:
|
||||||
|
network_class = "E (Reserved)"
|
||||||
|
|
||||||
|
# Calculate wildcard mask
|
||||||
|
subnet_mask_int = int(network.netmask)
|
||||||
|
wildcard_int = (2**32 - 1) ^ subnet_mask_int
|
||||||
|
wildcard_mask = str(ipaddress.IPv4Address(wildcard_int))
|
||||||
|
|
||||||
|
# Get first and last usable IPs - Calculate mathematically to avoid memory issues
|
||||||
|
total_hosts = network.num_addresses
|
||||||
|
|
||||||
|
if cidr == 32:
|
||||||
|
# Single host
|
||||||
|
first_usable = str(network.network_address)
|
||||||
|
last_usable = str(network.network_address)
|
||||||
|
usable_hosts = 1
|
||||||
|
elif cidr == 31:
|
||||||
|
# Point-to-point network (RFC 3021)
|
||||||
|
first_usable = str(network.network_address)
|
||||||
|
last_usable = str(network.broadcast_address)
|
||||||
|
usable_hosts = 2
|
||||||
|
else:
|
||||||
|
# Calculate first and last usable IPs mathematically
|
||||||
|
# First usable = network address + 1
|
||||||
|
# Last usable = broadcast address - 1
|
||||||
|
network_int = int(network.network_address)
|
||||||
|
broadcast_int = int(network.broadcast_address)
|
||||||
|
|
||||||
|
first_usable = str(ipaddress.IPv4Address(network_int + 1))
|
||||||
|
last_usable = str(ipaddress.IPv4Address(broadcast_int - 1))
|
||||||
|
usable_hosts = total_hosts - 2 # Exclude network and broadcast addresses
|
||||||
|
|
||||||
|
# Helper function to convert IP to binary representation
|
||||||
|
def ip_to_binary(ip_str):
|
||||||
|
ip = ipaddress.IPv4Address(ip_str)
|
||||||
|
# Convert to 32-bit binary and format with dots every 8 bits
|
||||||
|
binary = bin(int(ip))[2:].zfill(32)
|
||||||
|
return f"{binary[0:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}"
|
||||||
|
|
||||||
|
# Determine IP type
|
||||||
|
if ip_obj.is_loopback:
|
||||||
|
ip_type = "Loopback"
|
||||||
|
elif ip_obj.is_multicast:
|
||||||
|
ip_type = "Multicast"
|
||||||
|
elif ip_obj.is_link_local:
|
||||||
|
ip_type = "Link-Local"
|
||||||
|
elif ip_obj.is_reserved:
|
||||||
|
ip_type = "Reserved"
|
||||||
|
else:
|
||||||
|
ip_type = "Unicast"
|
||||||
|
|
||||||
|
return SubnetInfo(
|
||||||
|
ip_address=subnet_input.ip_address,
|
||||||
|
ip_address_binary=ip_to_binary(subnet_input.ip_address),
|
||||||
|
cidr=cidr,
|
||||||
|
subnet_mask=str(network.netmask),
|
||||||
|
subnet_mask_binary=ip_to_binary(str(network.netmask)),
|
||||||
|
network_id=str(network.network_address),
|
||||||
|
network_id_binary=ip_to_binary(str(network.network_address)),
|
||||||
|
broadcast_address=str(network.broadcast_address),
|
||||||
|
broadcast_binary=ip_to_binary(str(network.broadcast_address)),
|
||||||
|
first_usable_ip=first_usable,
|
||||||
|
last_usable_ip=last_usable,
|
||||||
|
total_hosts=network.num_addresses,
|
||||||
|
usable_hosts=usable_hosts,
|
||||||
|
wildcard_mask=wildcard_mask,
|
||||||
|
wildcard_binary=ip_to_binary(wildcard_mask),
|
||||||
|
network_class=network_class,
|
||||||
|
is_private=ip_obj.is_private,
|
||||||
|
ip_type=ip_type
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
# Re-raise HTTP exceptions as-is
|
||||||
|
raise
|
||||||
|
except ValueError as e:
|
||||||
|
# Pydantic validation errors
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
# Unexpected errors
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Unexpected error during calculation: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health_check():
|
||||||
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
3
backend/requirements.txt
Normal file
3
backend/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
pydantic==2.5.0
|
||||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ip-calculator-backend
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ip-calculator-frontend
|
||||||
|
ports:
|
||||||
|
- "3000:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:80"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
30
frontend/Dockerfile
Normal file
30
frontend/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:18-alpine as build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built files from build stage
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>IP Subnet Calculator</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
frontend/nginx.conf
Normal file
14
frontend/nginx.conf
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
}
|
||||||
1813
frontend/package-lock.json
generated
Normal file
1813
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "ip-calculator-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"axios": "^1.6.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
515
frontend/src/App.css
Normal file
515
frontend/src/App.css
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
.app {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #667eea;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto 30px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-box {
|
||||||
|
background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
|
||||||
|
border: 2px dashed #667eea;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-box label {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-box .input-hint {
|
||||||
|
background: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid #667eea;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-type-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn.active {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculate-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculate-btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculate-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculate-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: #fee;
|
||||||
|
border: 2px solid #fcc;
|
||||||
|
color: #c33;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailed-error {
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-reason {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-info {
|
||||||
|
background: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-info > div {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-suggestions {
|
||||||
|
background: #e8f4fd;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #2196F3;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-suggestions strong {
|
||||||
|
color: #1976D2;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-suggestions ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-suggestions li {
|
||||||
|
padding: 4px 0;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-message {
|
||||||
|
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||||
|
border: 2px solid #ffc107;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: flex-start;
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-content strong {
|
||||||
|
display: block;
|
||||||
|
color: #856404;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text {
|
||||||
|
color: #664d03;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 6px 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
margin-top: 40px;
|
||||||
|
animation: slideIn 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #667eea;
|
||||||
|
margin: 30px 0 15px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-card {
|
||||||
|
background: #f0f4ff;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-decimal {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-value {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.highlight {
|
||||||
|
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.division-summary {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subnets-table,
|
||||||
|
.ranges-table {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranges-table .table-header {
|
||||||
|
grid-template-columns: 60px 2fr 3fr 2fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranges-table .table-body {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranges-table .table-row {
|
||||||
|
grid-template-columns: 60px 2fr 3fr 2fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-address {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-id {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667eea;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 2fr 2.5fr 1.5fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 2fr 2.5fr 1.5fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subnet-id {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667eea;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subnet-network {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header,
|
||||||
|
.table-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
362
frontend/src/App.jsx
Normal file
362
frontend/src/App.jsx
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:8000'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [ipAddress, setIpAddress] = useState('')
|
||||||
|
const [cidr, setCidr] = useState('24')
|
||||||
|
const [subnetMask, setSubnetMask] = useState('')
|
||||||
|
const [inputType, setInputType] = useState('cidr') // 'cidr' or 'mask'
|
||||||
|
const [result, setResult] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
// Validate IP address format
|
||||||
|
const validateIpAddress = (ip) => {
|
||||||
|
if (!ip || !ip.trim()) {
|
||||||
|
return 'IP address is required'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipPattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
|
||||||
|
if (!ipPattern.test(ip)) {
|
||||||
|
return 'Invalid IP format. Expected: xxx.xxx.xxx.xxx'
|
||||||
|
}
|
||||||
|
|
||||||
|
const octets = ip.split('.')
|
||||||
|
for (let i = 0; i < octets.length; i++) {
|
||||||
|
const num = parseInt(octets[i])
|
||||||
|
if (isNaN(num) || num < 0 || num > 255) {
|
||||||
|
return `Invalid octet ${i + 1}: must be between 0-255`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate subnet mask format
|
||||||
|
const validateSubnetMask = (mask) => {
|
||||||
|
if (!mask || !mask.trim()) {
|
||||||
|
return 'Subnet mask is required'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = mask.split('.')
|
||||||
|
if (parts.length !== 4) {
|
||||||
|
return 'Subnet mask must have 4 octets'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each octet is 0-255
|
||||||
|
for (let part of parts) {
|
||||||
|
const num = parseInt(part)
|
||||||
|
if (isNaN(num) || num < 0 || num > 255) {
|
||||||
|
return 'Each octet must be between 0-255'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common valid subnet masks
|
||||||
|
const validMasks = [
|
||||||
|
'255.255.255.255', '255.255.255.254', '255.255.255.252', '255.255.255.248',
|
||||||
|
'255.255.255.240', '255.255.255.224', '255.255.255.192', '255.255.255.128',
|
||||||
|
'255.255.255.0', '255.255.254.0', '255.255.252.0', '255.255.248.0',
|
||||||
|
'255.255.240.0', '255.255.224.0', '255.255.192.0', '255.255.128.0',
|
||||||
|
'255.255.0.0', '255.254.0.0', '255.252.0.0', '255.248.0.0',
|
||||||
|
'255.240.0.0', '255.224.0.0', '255.192.0.0', '255.128.0.0',
|
||||||
|
'255.0.0.0', '254.0.0.0', '252.0.0.0', '248.0.0.0',
|
||||||
|
'240.0.0.0', '224.0.0.0', '192.0.0.0', '128.0.0.0', '0.0.0.0'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!validMasks.includes(mask)) {
|
||||||
|
return 'Invalid subnet mask (bits must be contiguous)'
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateSubnet = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setResult(null)
|
||||||
|
|
||||||
|
// Frontend validation
|
||||||
|
const ipError = validateIpAddress(ipAddress)
|
||||||
|
if (ipError) {
|
||||||
|
setError(ipError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputType === 'cidr') {
|
||||||
|
const cidrNum = parseInt(cidr)
|
||||||
|
if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) {
|
||||||
|
setError('CIDR must be between 0 and 32')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const maskError = validateSubnetMask(subnetMask)
|
||||||
|
if (maskError) {
|
||||||
|
setError(maskError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
ip_address: ipAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputType === 'cidr') {
|
||||||
|
payload.cidr = parseInt(cidr)
|
||||||
|
} else {
|
||||||
|
payload.subnet_mask = subnetMask
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(`${API_URL}/calculate`, payload)
|
||||||
|
setResult(response.data)
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to calculate subnet'
|
||||||
|
setError(errorMessage)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const InfoCard = ({ label, value, highlight }) => (
|
||||||
|
<div className={`info-card ${highlight ? 'highlight' : ''}`}>
|
||||||
|
<div className="info-label">{label}</div>
|
||||||
|
<div className="info-value">{value}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const BinaryCard = ({ label, decimal, binary }) => (
|
||||||
|
<div className="binary-card">
|
||||||
|
<div className="binary-label">{label}</div>
|
||||||
|
<div className="binary-decimal">{decimal}</div>
|
||||||
|
<div className="binary-value">{binary}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate ALL next sequential networks based on the current network
|
||||||
|
const calculateNetworkRanges = (result) => {
|
||||||
|
const networkId = result.network_id
|
||||||
|
const cidr = result.cidr
|
||||||
|
const totalAddresses = result.total_hosts
|
||||||
|
|
||||||
|
// Parse network ID to get starting IP
|
||||||
|
const parts = networkId.split('.').map(Number)
|
||||||
|
let networkNum = (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
|
||||||
|
|
||||||
|
const networks = []
|
||||||
|
|
||||||
|
// Calculate all possible networks until end of IPv4 space
|
||||||
|
// For performance, limit to 10000 networks max
|
||||||
|
const maxNetworks = 10000
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
// Generate next sequential networks
|
||||||
|
while (count < maxNetworks) {
|
||||||
|
const currentNetworkNum = networkNum + (count * totalAddresses)
|
||||||
|
|
||||||
|
// Check if we're still within valid IPv4 range
|
||||||
|
const broadcastNum = currentNetworkNum + totalAddresses - 1
|
||||||
|
if (currentNetworkNum > 0xFFFFFFFF || broadcastNum > 0xFFFFFFFF) break
|
||||||
|
|
||||||
|
// Convert number back to IP address
|
||||||
|
const networkAddress = `${(currentNetworkNum >>> 24) & 0xFF}.${(currentNetworkNum >>> 16) & 0xFF}.${(currentNetworkNum >>> 8) & 0xFF}.${currentNetworkNum & 0xFF}`
|
||||||
|
const broadcastAddress = `${(broadcastNum >>> 24) & 0xFF}.${(broadcastNum >>> 16) & 0xFF}.${(broadcastNum >>> 8) & 0xFF}.${broadcastNum & 0xFF}`
|
||||||
|
|
||||||
|
// Calculate first and last usable IPs
|
||||||
|
let firstUsable, lastUsable
|
||||||
|
if (cidr === 32) {
|
||||||
|
firstUsable = networkAddress
|
||||||
|
lastUsable = networkAddress
|
||||||
|
} else if (cidr === 31) {
|
||||||
|
firstUsable = networkAddress
|
||||||
|
lastUsable = broadcastAddress
|
||||||
|
} else {
|
||||||
|
const firstUsableNum = currentNetworkNum + 1
|
||||||
|
const lastUsableNum = broadcastNum - 1
|
||||||
|
firstUsable = `${(firstUsableNum >>> 24) & 0xFF}.${(firstUsableNum >>> 16) & 0xFF}.${(firstUsableNum >>> 8) & 0xFF}.${firstUsableNum & 0xFF}`
|
||||||
|
lastUsable = `${(lastUsableNum >>> 24) & 0xFF}.${(lastUsableNum >>> 16) & 0xFF}.${(lastUsableNum >>> 8) & 0xFF}.${lastUsableNum & 0xFF}`
|
||||||
|
}
|
||||||
|
|
||||||
|
networks.push({
|
||||||
|
networkAddress: `${networkAddress}/${cidr}`,
|
||||||
|
firstUsable,
|
||||||
|
lastUsable,
|
||||||
|
broadcastAddress,
|
||||||
|
usableHosts: result.usable_hosts
|
||||||
|
})
|
||||||
|
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
return networks
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<div className="container">
|
||||||
|
<h1 className="title">🌐 IP Subnet Calculator</h1>
|
||||||
|
|
||||||
|
<form onSubmit={calculateSubnet} className="form">
|
||||||
|
<div className="input-group">
|
||||||
|
<label htmlFor="ipAddress">IP Address</label>
|
||||||
|
<input
|
||||||
|
id="ipAddress"
|
||||||
|
type="text"
|
||||||
|
value={ipAddress}
|
||||||
|
onChange={(e) => setIpAddress(e.target.value)}
|
||||||
|
placeholder="e.g., 192.168.1.1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-type-selector">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`type-btn ${inputType === 'cidr' ? 'active' : ''}`}
|
||||||
|
onClick={() => setInputType('cidr')}
|
||||||
|
>
|
||||||
|
CIDR Notation
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`type-btn ${inputType === 'mask' ? 'active' : ''}`}
|
||||||
|
onClick={() => setInputType('mask')}
|
||||||
|
>
|
||||||
|
Subnet Mask
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{inputType === 'cidr' ? (
|
||||||
|
<div className="input-group">
|
||||||
|
<label htmlFor="cidr">CIDR (1-32)</label>
|
||||||
|
<input
|
||||||
|
id="cidr"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="32"
|
||||||
|
value={cidr}
|
||||||
|
onChange={(e) => setCidr(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="input-group">
|
||||||
|
<label htmlFor="subnetMask">Subnet Mask</label>
|
||||||
|
<input
|
||||||
|
id="subnetMask"
|
||||||
|
type="text"
|
||||||
|
value={subnetMask}
|
||||||
|
onChange={(e) => setSubnetMask(e.target.value)}
|
||||||
|
placeholder="e.g., 255.255.255.0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button type="submit" className="calculate-btn" disabled={loading}>
|
||||||
|
{loading ? 'Calculating...' : 'Calculate Subnet'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="error-message">
|
||||||
|
⚠️ {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div className="results">
|
||||||
|
<h2 className="results-title">Subnet Information</h2>
|
||||||
|
|
||||||
|
{/* Binary Representation Section */}
|
||||||
|
<div className="binary-section">
|
||||||
|
<h3 className="section-title">Binary Representation</h3>
|
||||||
|
<div className="binary-grid">
|
||||||
|
<BinaryCard
|
||||||
|
label="Address"
|
||||||
|
decimal={result.ip_address}
|
||||||
|
binary={result.ip_address_binary}
|
||||||
|
/>
|
||||||
|
<BinaryCard
|
||||||
|
label="Netmask"
|
||||||
|
decimal={`${result.subnet_mask} = /${result.cidr}`}
|
||||||
|
binary={result.subnet_mask_binary}
|
||||||
|
/>
|
||||||
|
<BinaryCard
|
||||||
|
label="Wildcard"
|
||||||
|
decimal={result.wildcard_mask}
|
||||||
|
binary={result.wildcard_binary}
|
||||||
|
/>
|
||||||
|
<BinaryCard
|
||||||
|
label="Network"
|
||||||
|
decimal={`${result.network_id}/${result.cidr}`}
|
||||||
|
binary={result.network_id_binary}
|
||||||
|
/>
|
||||||
|
<BinaryCard
|
||||||
|
label="Broadcast"
|
||||||
|
decimal={result.broadcast_address}
|
||||||
|
binary={result.broadcast_binary}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Information Section */}
|
||||||
|
<h3 className="section-title">Network Details</h3>
|
||||||
|
<div className="results-grid">
|
||||||
|
<InfoCard label="Network ID" value={`${result.network_id}/${result.cidr}`} highlight />
|
||||||
|
<InfoCard label="Broadcast Address" value={result.broadcast_address} highlight />
|
||||||
|
|
||||||
|
<InfoCard label="First Usable IP" value={result.first_usable_ip} />
|
||||||
|
<InfoCard label="Last Usable IP" value={result.last_usable_ip} />
|
||||||
|
|
||||||
|
<InfoCard label="Total Addresses" value={result.total_hosts.toLocaleString()} highlight />
|
||||||
|
<InfoCard label="Usable Hosts" value={result.usable_hosts.toLocaleString()} highlight />
|
||||||
|
|
||||||
|
<InfoCard label="Subnet Mask" value={result.subnet_mask} />
|
||||||
|
<InfoCard label="Wildcard Mask" value={result.wildcard_mask} />
|
||||||
|
|
||||||
|
<InfoCard label="Network Class" value={result.network_class} />
|
||||||
|
<InfoCard label="IP Type" value={result.ip_type} />
|
||||||
|
|
||||||
|
<InfoCard
|
||||||
|
label="Private IP"
|
||||||
|
value={result.is_private ? '✅ Yes' : '❌ No'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Next Networks Section */}
|
||||||
|
<h3 className="section-title">
|
||||||
|
📊 All Next Sequential Networks ({calculateNetworkRanges(result).length.toLocaleString()} networks)
|
||||||
|
</h3>
|
||||||
|
<div className="ranges-table">
|
||||||
|
<div className="table-header">
|
||||||
|
<div className="table-cell">#</div>
|
||||||
|
<div className="table-cell">Network</div>
|
||||||
|
<div className="table-cell">Usable Range</div>
|
||||||
|
<div className="table-cell">Broadcast</div>
|
||||||
|
<div className="table-cell">Hosts</div>
|
||||||
|
</div>
|
||||||
|
<div className="table-body">
|
||||||
|
{calculateNetworkRanges(result).map((network, index) => (
|
||||||
|
<div key={index} className="table-row">
|
||||||
|
<div className="table-cell range-id">{index + 1}</div>
|
||||||
|
<div className="table-cell network-address">{network.networkAddress}</div>
|
||||||
|
<div className="table-cell">{network.firstUsable} - {network.lastUsable}</div>
|
||||||
|
<div className="table-cell">{network.broadcastAddress}</div>
|
||||||
|
<div className="table-cell">{network.usableHosts.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
22
frontend/src/index.css
Normal file
22
frontend/src/index.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
292
frontend/src/subnetCalculator.js
Normal file
292
frontend/src/subnetCalculator.js
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
/**
|
||||||
|
* Pure subnet calculation utilities for IPv4
|
||||||
|
* No DOM/UI dependencies - pure mathematical functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert IPv4 string to 32-bit unsigned integer
|
||||||
|
* @param {string} ipStr - IPv4 address (e.g., "192.168.10.0")
|
||||||
|
* @returns {number} 32-bit unsigned integer
|
||||||
|
* @throws {Error} if invalid IPv4 format
|
||||||
|
*
|
||||||
|
* Example: ipToInt("192.168.10.0") => 3232238080
|
||||||
|
*/
|
||||||
|
export function ipToInt(ipStr) {
|
||||||
|
if (!ipStr || typeof ipStr !== 'string') {
|
||||||
|
throw new Error('IP address must be a non-empty string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const octets = ipStr.split('.')
|
||||||
|
if (octets.length !== 4) {
|
||||||
|
throw new Error(`Invalid IPv4 format: ${ipStr}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = 0
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const octet = parseInt(octets[i], 10)
|
||||||
|
if (isNaN(octet) || octet < 0 || octet > 255) {
|
||||||
|
throw new Error(`Invalid octet in IP ${ipStr}: ${octets[i]}`)
|
||||||
|
}
|
||||||
|
result = (result << 8) | octet
|
||||||
|
}
|
||||||
|
|
||||||
|
return result >>> 0 // Ensure unsigned 32-bit
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert 32-bit unsigned integer to IPv4 string
|
||||||
|
* @param {number} ipInt - 32-bit unsigned integer
|
||||||
|
* @returns {string} IPv4 address
|
||||||
|
*
|
||||||
|
* Example: intToIp(3232238080) => "192.168.10.0"
|
||||||
|
*/
|
||||||
|
export function intToIp(ipInt) {
|
||||||
|
const uint = ipInt >>> 0 // Ensure unsigned
|
||||||
|
const octet1 = (uint >>> 24) & 0xFF
|
||||||
|
const octet2 = (uint >>> 16) & 0xFF
|
||||||
|
const octet3 = (uint >>> 8) & 0xFF
|
||||||
|
const octet4 = uint & 0xFF
|
||||||
|
return `${octet1}.${octet2}.${octet3}.${octet4}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate subnet mask as 32-bit integer from prefix length
|
||||||
|
* @param {number} prefixLength - CIDR prefix (0-32)
|
||||||
|
* @returns {number} 32-bit subnet mask
|
||||||
|
*
|
||||||
|
* Example: maskFromPrefix(24) => 4294967040 (255.255.255.0)
|
||||||
|
*/
|
||||||
|
export function maskFromPrefix(prefixLength) {
|
||||||
|
if (prefixLength < 0 || prefixLength > 32) {
|
||||||
|
throw new Error(`Prefix length must be between 0 and 32, got: ${prefixLength}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefixLength === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0xFFFFFFFF << (32 - prefixLength)) >>> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the smallest prefix length needed for desired usable hosts
|
||||||
|
* For IPv4: usable hosts = 2^h - 2 (excluding network and broadcast)
|
||||||
|
*
|
||||||
|
* @param {number} desiredUsableHosts - Number of usable hosts needed
|
||||||
|
* @returns {number} CIDR prefix length (0-30)
|
||||||
|
* @throws {Error} if invalid input
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* prefixFromHosts(50) => 26 (2^6 - 2 = 62 usable hosts)
|
||||||
|
* prefixFromHosts(10) => 28 (2^4 - 2 = 14 usable hosts)
|
||||||
|
* prefixFromHosts(1) => 30 (2^2 - 2 = 2 usable hosts)
|
||||||
|
* prefixFromHosts(254) => 24 (2^8 - 2 = 254 usable hosts)
|
||||||
|
*/
|
||||||
|
export function prefixFromHosts(desiredUsableHosts) {
|
||||||
|
if (!Number.isInteger(desiredUsableHosts) || desiredUsableHosts < 1) {
|
||||||
|
throw new Error(`Desired usable hosts must be a positive integer, got: ${desiredUsableHosts}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximum usable hosts in IPv4 is 2^30 - 2 (prefix /2)
|
||||||
|
if (desiredUsableHosts > 1073741822) {
|
||||||
|
throw new Error(`Desired hosts ${desiredUsableHosts} exceeds IPv4 maximum (1,073,741,822)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find smallest h where 2^h - 2 >= desiredUsableHosts
|
||||||
|
// This means 2^h >= desiredUsableHosts + 2
|
||||||
|
const requiredTotal = desiredUsableHosts + 2
|
||||||
|
|
||||||
|
// Find h: smallest integer where 2^h >= requiredTotal
|
||||||
|
let h = 0
|
||||||
|
while ((1 << h) < requiredTotal) {
|
||||||
|
h++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix length = 32 - h
|
||||||
|
const prefix = 32 - h
|
||||||
|
|
||||||
|
// Ensure we don't return /31 or /32 (no usable hosts in standard model)
|
||||||
|
if (prefix > 30) {
|
||||||
|
return 30 // /30 gives 2 usable hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Align an IP address to the network boundary for a given prefix
|
||||||
|
* @param {number} ipInt - IP address as 32-bit integer
|
||||||
|
* @param {number} prefixLength - CIDR prefix length
|
||||||
|
* @returns {number} Aligned network address
|
||||||
|
*
|
||||||
|
* Example: alignToNetwork(ipToInt("192.168.10.5"), 24) => ipToInt("192.168.10.0")
|
||||||
|
*/
|
||||||
|
export function alignToNetwork(ipInt, prefixLength) {
|
||||||
|
const mask = maskFromPrefix(prefixLength)
|
||||||
|
return (ipInt & mask) >>> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate subnets based on base network, desired hosts per subnet, and subnet count
|
||||||
|
*
|
||||||
|
* @param {string} baseNetwork - Base IPv4 address (e.g., "192.168.10.0")
|
||||||
|
* @param {number} desiredUsableHosts - Number of usable hosts needed per subnet
|
||||||
|
* @param {number} subnetCount - Number of subnets to generate
|
||||||
|
* @returns {Object} Subnet calculation results
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* calculateSubnets("192.168.10.0", 50, 4)
|
||||||
|
* => {
|
||||||
|
* prefixLength: 26,
|
||||||
|
* subnetMask: "255.255.255.192",
|
||||||
|
* totalAddressesPerSubnet: 64,
|
||||||
|
* usableHostsPerSubnet: 62,
|
||||||
|
* alignedBaseNetwork: "192.168.10.0",
|
||||||
|
* warnings: [],
|
||||||
|
* subnets: [
|
||||||
|
* { index: 0, network: "192.168.10.0/26", firstUsable: "192.168.10.1",
|
||||||
|
* lastUsable: "192.168.10.62", broadcast: "192.168.10.63" },
|
||||||
|
* { index: 1, network: "192.168.10.64/26", firstUsable: "192.168.10.65",
|
||||||
|
* lastUsable: "192.168.10.126", broadcast: "192.168.10.127" },
|
||||||
|
* ...
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function calculateSubnets(baseNetwork, desiredUsableHosts, subnetCount) {
|
||||||
|
const warnings = []
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
if (!baseNetwork || typeof baseNetwork !== 'string') {
|
||||||
|
throw new Error('Base network must be a valid IPv4 string')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(desiredUsableHosts) || desiredUsableHosts < 1) {
|
||||||
|
throw new Error('Desired usable hosts must be a positive integer')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(subnetCount) || subnetCount < 1) {
|
||||||
|
throw new Error('Subnet count must be a positive integer')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert base network to integer
|
||||||
|
let baseInt
|
||||||
|
try {
|
||||||
|
baseInt = ipToInt(baseNetwork)
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Invalid base network: ${err.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate required prefix length
|
||||||
|
const prefixLength = prefixFromHosts(desiredUsableHosts)
|
||||||
|
|
||||||
|
if (prefixLength < 0 || prefixLength > 30) {
|
||||||
|
throw new Error(`Calculated prefix length ${prefixLength} is out of valid range (0-30)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate subnet properties
|
||||||
|
const hostBits = 32 - prefixLength
|
||||||
|
const totalAddressesPerSubnet = Math.pow(2, hostBits)
|
||||||
|
const usableHostsPerSubnet = totalAddressesPerSubnet - 2 // Exclude network and broadcast
|
||||||
|
const subnetMaskInt = maskFromPrefix(prefixLength)
|
||||||
|
const subnetMask = intToIp(subnetMaskInt)
|
||||||
|
|
||||||
|
// Align base network to proper boundary
|
||||||
|
const alignedBaseInt = alignToNetwork(baseInt, prefixLength)
|
||||||
|
const alignedBaseNetwork = intToIp(alignedBaseInt)
|
||||||
|
|
||||||
|
if (alignedBaseInt !== baseInt) {
|
||||||
|
warnings.push(
|
||||||
|
`Base network ${baseNetwork} is not aligned to /${prefixLength} boundary. ` +
|
||||||
|
`Adjusted to ${alignedBaseNetwork}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate subnets
|
||||||
|
const subnets = []
|
||||||
|
const maxIpInt = 0xFFFFFFFF // Maximum IPv4 address
|
||||||
|
|
||||||
|
for (let i = 0; i < subnetCount; i++) {
|
||||||
|
const networkInt = alignedBaseInt + (i * totalAddressesPerSubnet)
|
||||||
|
|
||||||
|
// Check for overflow
|
||||||
|
if (networkInt > maxIpInt) {
|
||||||
|
warnings.push(
|
||||||
|
`Subnet generation stopped at index ${i} due to IPv4 address space overflow. ` +
|
||||||
|
`Requested ${subnetCount} subnets but only ${i} can fit.`
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcastInt = networkInt + totalAddressesPerSubnet - 1
|
||||||
|
|
||||||
|
// Check if broadcast would overflow
|
||||||
|
if (broadcastInt > maxIpInt) {
|
||||||
|
warnings.push(
|
||||||
|
`Subnet ${i} would exceed IPv4 address space. ` +
|
||||||
|
`Generated ${i} subnets out of ${subnetCount} requested.`
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstUsableInt = networkInt + 1
|
||||||
|
const lastUsableInt = broadcastInt - 1
|
||||||
|
|
||||||
|
subnets.push({
|
||||||
|
index: i,
|
||||||
|
network: `${intToIp(networkInt)}/${prefixLength}`,
|
||||||
|
networkAddress: intToIp(networkInt),
|
||||||
|
firstUsable: intToIp(firstUsableInt),
|
||||||
|
lastUsable: intToIp(lastUsableInt),
|
||||||
|
broadcast: intToIp(broadcastInt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
prefixLength,
|
||||||
|
subnetMask,
|
||||||
|
totalAddressesPerSubnet,
|
||||||
|
usableHostsPerSubnet,
|
||||||
|
alignedBaseNetwork,
|
||||||
|
warnings,
|
||||||
|
subnets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test examples (can be run in console or test suite)
|
||||||
|
*/
|
||||||
|
export const testExamples = {
|
||||||
|
// Example 1: 50 hosts => /26
|
||||||
|
example1: () => {
|
||||||
|
console.log('Example 1: 50 hosts per subnet')
|
||||||
|
const result = calculateSubnets('192.168.10.0', 50, 4)
|
||||||
|
console.log('Prefix:', result.prefixLength) // 26
|
||||||
|
console.log('Subnet Mask:', result.subnetMask) // 255.255.255.192
|
||||||
|
console.log('Usable Hosts:', result.usableHostsPerSubnet) // 62
|
||||||
|
console.log('First subnet:', result.subnets[0])
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
// Example 2: Unaligned base network
|
||||||
|
example2: () => {
|
||||||
|
console.log('Example 2: Unaligned base network')
|
||||||
|
const result = calculateSubnets('192.168.10.5', 10, 2)
|
||||||
|
console.log('Aligned to:', result.alignedBaseNetwork) // 192.168.10.0
|
||||||
|
console.log('Warnings:', result.warnings)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
// Example 3: Large subnet
|
||||||
|
example3: () => {
|
||||||
|
console.log('Example 3: 1000 hosts per subnet')
|
||||||
|
const result = calculateSubnets('10.0.0.0', 1000, 3)
|
||||||
|
console.log('Prefix:', result.prefixLength) // 22
|
||||||
|
console.log('Usable Hosts:', result.usableHostsPerSubnet) // 1022
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to run examples:
|
||||||
|
// testExamples.example1()
|
||||||
|
// testExamples.example2()
|
||||||
|
// testExamples.example3()
|
||||||
9
frontend/vite.config.js
Normal file
9
frontend/vite.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
})
|
||||||
72
values.yaml
Normal file
72
values.yaml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Basic configuration values for IP Calculator application
|
||||||
|
|
||||||
|
# Application metadata
|
||||||
|
app:
|
||||||
|
name: ip-calculator
|
||||||
|
version: "1.0.0"
|
||||||
|
description: "IP Subnet Calculator with React + Vite frontend and FastAPI backend"
|
||||||
|
|
||||||
|
# Backend configuration
|
||||||
|
backend:
|
||||||
|
port: 8000
|
||||||
|
host: "0.0.0.0"
|
||||||
|
apiUrl: "http://localhost:8000"
|
||||||
|
cors:
|
||||||
|
enabled: true
|
||||||
|
origins:
|
||||||
|
- "http://localhost:3000"
|
||||||
|
- "http://localhost:5173"
|
||||||
|
|
||||||
|
# Frontend configuration
|
||||||
|
frontend:
|
||||||
|
port: 3000
|
||||||
|
apiUrl: "http://localhost:8000"
|
||||||
|
|
||||||
|
# Development settings
|
||||||
|
development:
|
||||||
|
hotReload: true
|
||||||
|
debug: true
|
||||||
|
|
||||||
|
# Production settings
|
||||||
|
production:
|
||||||
|
debug: false
|
||||||
|
minify: true
|
||||||
|
|
||||||
|
# Docker configuration
|
||||||
|
docker:
|
||||||
|
backend:
|
||||||
|
image: "ip-calculator-backend"
|
||||||
|
tag: "latest"
|
||||||
|
frontend:
|
||||||
|
image: "ip-calculator-frontend"
|
||||||
|
tag: "latest"
|
||||||
|
|
||||||
|
# Kubernetes/Helm configuration (for helm chart)
|
||||||
|
kubernetes:
|
||||||
|
namespace: "default"
|
||||||
|
replicas:
|
||||||
|
backend: 1
|
||||||
|
frontend: 1
|
||||||
|
resources:
|
||||||
|
backend:
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "512Mi"
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "256Mi"
|
||||||
|
frontend:
|
||||||
|
limits:
|
||||||
|
cpu: "200m"
|
||||||
|
memory: "256Mi"
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "128Mi"
|
||||||
|
|
||||||
|
# Ingress configuration
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
host: "ip-calculator.example.com"
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
secretName: "ip-calculator-tls"
|
||||||
Loading…
x
Reference in New Issue
Block a user