This commit is contained in:
pika 2025-04-03 16:58:01 +02:00
parent 2b36992be1
commit 25087d055c
16 changed files with 1394 additions and 816 deletions

View file

@ -58,21 +58,14 @@ def create_app(config_name="development"):
print(f"Error with database setup: {e}")
# Register blueprints
from app.routes.auth import bp as auth_bp
from app.routes import auth, dashboard, ipam
app.register_blueprint(auth.bp)
app.register_blueprint(dashboard.bp)
app.register_blueprint(ipam.bp)
app.register_blueprint(auth_bp)
from app.routes.dashboard import bp as dashboard_bp
app.register_blueprint(dashboard_bp)
from app.routes.ipam import bp as ipam_bp
app.register_blueprint(ipam_bp)
from app.routes.api import bp as api_bp
app.register_blueprint(api_bp)
# Register API routes
from app.routes import api
app.register_blueprint(api.bp)
from app.routes.importexport import bp as importexport_bp

View file

@ -7,17 +7,23 @@ login_manager = LoginManager()
login_manager.login_view = "auth.login"
class User(UserMixin, db.Model):
class User(db.Model, UserMixin):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=True)
username = db.Column(db.String(64), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
# User's assets
locations = db.relationship("Location", backref="owner", lazy=True, cascade="all, delete-orphan")
subnets = db.relationship("Subnet", backref="owner", lazy=True, cascade="all, delete-orphan")
servers = db.relationship("Server", backref="owner", lazy=True, cascade="all, delete-orphan")
apps = db.relationship("App", backref="owner", lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f"<User {self.username}>"

View file

@ -2,68 +2,49 @@ from app.core.extensions import db
import json
from datetime import datetime
import ipaddress
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
# User model has been moved to app.core.auth
# Import it from there instead if needed: from app.core.auth import User
# Import User from auth instead of defining it here
from app.core.auth import User
class Port(db.Model):
__tablename__ = "ports"
class Location(db.Model):
__tablename__ = "locations"
id = db.Column(db.Integer, primary_key=True)
app_id = db.Column(
db.Integer, db.ForeignKey("apps.id", ondelete="CASCADE"), nullable=False
)
port_number = db.Column(db.Integer, nullable=False)
protocol = db.Column(db.String(10), default="TCP") # TCP, UDP, etc.
description = db.Column(db.String(200))
# Relationship
app = db.relationship("App", back_populates="ports")
def __repr__(self):
return f"<Port {self.port_number}/{self.protocol}>"
class Server(db.Model):
__tablename__ = "servers"
id = db.Column(db.Integer, primary_key=True)
hostname = db.Column(db.String(64), nullable=False)
ip_address = db.Column(db.String(39), nullable=False) # IPv4 or IPv6
subnet_id = db.Column(db.Integer, db.ForeignKey("subnets.id"), nullable=False)
documentation = db.Column(db.Text)
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
subnet = db.relationship("Subnet", back_populates="servers")
apps = db.relationship("App", back_populates="server", cascade="all, delete-orphan")
subnets = db.relationship("Subnet", backref="location_ref", lazy=True, cascade="all, delete-orphan")
standalone_servers = db.relationship(
"Server",
primaryjoin="and_(Server.location_id==Location.id, Server.subnet_id==None)",
backref="location_ref",
lazy=True
)
def __repr__(self):
return f"<Server {self.hostname}>"
return f"<Location {self.name}>"
class Subnet(db.Model):
__tablename__ = "subnets"
id = db.Column(db.Integer, primary_key=True)
cidr = db.Column(db.String(18), unique=True, nullable=False) # e.g., 192.168.1.0/24
location = db.Column(db.String(64))
active_hosts = db.Column(db.Text) # Store as JSON string
last_scanned = db.Column(db.DateTime)
cidr = db.Column(db.String(45), nullable=False)
location_id = db.Column(db.Integer, db.ForeignKey("locations.id"), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
auto_scan = db.Column(db.Boolean, default=False)
active_hosts = db.Column(db.Text, default='[]')
last_scanned = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
servers = db.relationship("Server", back_populates="subnet")
servers = db.relationship("Server", backref="subnet", lazy=True)
def __repr__(self):
return f"<Subnet {self.cidr}>"
@ -85,22 +66,63 @@ class Subnet(db.Model):
self.active_hosts = json.dumps(hosts)
class Server(db.Model):
__tablename__ = "servers"
id = db.Column(db.Integer, primary_key=True)
hostname = db.Column(db.String(255), nullable=False)
ip_address = db.Column(db.String(45), nullable=False)
subnet_id = db.Column(db.Integer, db.ForeignKey("subnets.id"), nullable=True)
location_id = db.Column(db.Integer, db.ForeignKey("locations.id"), nullable=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
description = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
apps = db.relationship("App", back_populates="server", lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f"<Server {self.hostname}>"
class App(db.Model):
__tablename__ = "apps"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), nullable=False)
server_id = db.Column(db.Integer, db.ForeignKey("servers.id"), nullable=False)
documentation = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
documentation = db.Column(db.Text, nullable=True)
url = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
server = db.relationship("Server", back_populates="apps")
ports = db.relationship("Port", back_populates="app", cascade="all, delete-orphan")
ports = db.relationship("Port", back_populates="app", lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f"<App {self.name}>"
class Port(db.Model):
__tablename__ = "ports"
id = db.Column(db.Integer, primary_key=True)
app_id = db.Column(db.Integer, db.ForeignKey("apps.id"), nullable=False)
port_number = db.Column(db.Integer, nullable=False)
protocol = db.Column(db.String(10), nullable=False, default="TCP")
description = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
app = db.relationship("App", back_populates="ports")
__table_args__ = (
db.UniqueConstraint('app_id', 'port_number', 'protocol', name='unique_port_per_app_protocol'),
)
def __repr__(self):
return f"<Port {self.port_number}/{self.protocol}>"

View file

@ -8,8 +8,8 @@ from flask import (
redirect,
url_for,
)
from flask_login import login_required
from app.core.models import Subnet, Server, App, Port
from flask_login import login_required, current_user
from app.core.models import Subnet, Server, App, Port, Location
from app.core.extensions import db
from app.scripts.ip_scanner import scan
import random
@ -20,33 +20,51 @@ from datetime import datetime
from flask import flash
from app.utils.app_utils import is_port_in_use, validate_port_data
from difflib import SequenceMatcher
import json
bp = Blueprint("api", __name__, url_prefix="/api")
csrf = CSRFProtect()
@bp.route("/subnets", methods=["GET"])
@csrf.exempt
@login_required
def get_subnets():
"""Get all subnets grouped by site"""
subnets = Subnet.query.all()
try:
subnets = Subnet.query.filter_by(user_id=current_user.id).all()
# Group subnets by location (site)
sites = {}
for subnet in subnets:
location = subnet.location
if location not in sites:
sites[location] = []
location = subnet.location_ref # Make sure this attribute matches your model relationship
if not location:
location_name = "Unassigned"
location_id = None
else:
location_name = location.name
location_id = location.id
sites[location].append(
{"id": subnet.id, "cidr": subnet.cidr, "location": location}
)
if location_id not in sites:
sites[location_id] = {
"name": location_name,
"id": location_id,
"subnets": []
}
sites[location_id]["subnets"].append({
"id": subnet.id,
"cidr": subnet.cidr,
"location_id": location_id
})
# Convert to list of site objects
result = [
{"name": site_name, "subnets": subnets} for site_name, subnets in sites.items()
]
result = list(sites.values())
return jsonify(result)
except Exception as e:
print(f"Error loading subnets: {e}") # Add debugging
return jsonify({"error": str(e)}), 500
@bp.route("/subnets/<int:subnet_id>", methods=["GET"])
@ -288,39 +306,59 @@ def add_app_port(app_id):
"""Add a port to an application"""
app = App.query.get_or_404(app_id)
# Check if the user owns this app
if app.user_id != current_user.id:
flash("Unauthorized access", "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id))
# Get port details from the form
port_number = request.form.get("port_number")
protocol = request.form.get("protocol", "TCP")
description = request.form.get("description", "")
# Validate the port
valid, clean_port, error = validate_port_data ( # validate_port_data(
valid, clean_port, error = validate_port_data(
port_number,
protocol,
description,
app.server_id,
app_id
descriptions=description,
server_id=app.server_id,
exclude_app_id=app_id,
protocol=protocol
)
if not valid:
flash(error, "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id))
# Create the new port
try:
new_port = Port(
# Check if the port already exists for this app
existing_port = Port.query.filter_by(
app_id=app_id,
port_number=clean_port,
protocol=protocol
).first()
if existing_port:
# Update the existing port description
existing_port.description = description
flash(f"Port {clean_port}/{protocol} updated", "success")
else:
# Create a new port
port = Port(
port_number=clean_port,
protocol=protocol,
description=description
description=description,
app_id=app_id
)
db.session.add(new_port)
db.session.commit()
db.session.add(port)
flash(f"Port {clean_port}/{protocol} added successfully", "success")
db.session.commit()
return redirect(url_for("dashboard.app_view", app_id=app_id))
except Exception as e:
db.session.rollback()
flash(f"Error adding port: {str(e)}", "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id))
@ -337,7 +375,7 @@ def get_app_ports(app_id):
"ports": [
{
"id": port.id,
"number": port.number,
"port_number": port.port_number,
"protocol": port.protocol,
"description": port.description,
}
@ -396,7 +434,7 @@ def get_server_ports(server_id):
# Get all ports associated with this server
ports = Port.query.filter_by(server_id=server_id).all()
used_ports = [port.number for port in ports]
used_ports = [port.port_number for port in ports]
return jsonify({"server_id": server_id, "used_ports": used_ports})
@ -409,7 +447,7 @@ def get_free_port(server_id):
# Get all ports associated with this server
used_ports = [
port.number for port in Port.query.filter_by(server_id=server_id).all()
port.port_number for port in Port.query.filter_by(server_id=server_id).all()
]
# Find the first free port (starting from 8000)
@ -524,3 +562,65 @@ def validate_app_port():
except ValueError:
return jsonify({"valid": False, "message": "Invalid port number"})
@bp.route("/locations", methods=["POST"])
@login_required
def create_location():
"""API endpoint to create a new location"""
data = request.json
if not data or not data.get('name'):
return jsonify({'error': 'Location name is required'}), 400
try:
location = Location(
name=data.get('name'),
description=data.get('description', ''),
user_id=current_user.id
)
db.session.add(location)
db.session.commit()
return jsonify({
'id': location.id,
'name': location.name,
'description': location.description
}), 201
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@bp.route("/subnets", methods=["POST"])
@login_required
def create_subnet():
"""API endpoint to create a new subnet"""
data = request.json
if not data or not data.get('cidr') or not data.get('location_id'):
return jsonify({'error': 'CIDR and location_id are required'}), 400
try:
subnet = Subnet(
cidr=data.get('cidr'),
location_id=data.get('location_id'),
user_id=current_user.id,
auto_scan=data.get('auto_scan', False),
active_hosts=json.dumps([])
)
db.session.add(subnet)
db.session.commit()
return jsonify({
'id': subnet.id,
'cidr': subnet.cidr,
'location_id': subnet.location_id
}), 201
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500

View file

@ -1,10 +1,10 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify, abort
from flask_login import login_required, current_user
import markdown
from app.core.models import Server, App, Subnet, Port
from app.core.models import Server, App, Subnet, Port, Location
from app.core.extensions import db, limiter
from datetime import datetime
from app.utils.app_utils import validate_app_data, is_port_in_use
from app.utils.app_utils import validate_app_data, is_port_in_use, validate_port_data
bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
@ -77,41 +77,35 @@ def server_view(server_id):
@login_required
def server_new():
"""Create a new server"""
subnets = Subnet.query.all()
# Get all subnets and locations for the current user
subnets = Subnet.query.filter_by(user_id=current_user.id).all()
locations = Location.query.filter_by(user_id=current_user.id).all()
if request.method == "POST":
hostname = request.form.get("hostname")
ip_address = request.form.get("ip_address")
subnet_id = request.form.get("subnet_id")
documentation = request.form.get("documentation", "")
subnet_id = request.form.get("subnet_id") or None
location_id = request.form.get("location_id") or None
description = request.form.get("description")
# Basic validation
if not hostname or not ip_address or not subnet_id:
flash("Please fill in all required fields", "danger")
# Validate inputs
if not hostname or not ip_address:
flash("Hostname and IP address are required", "danger")
return render_template(
"dashboard/server_form.html",
title="New Server",
subnets=subnets,
now=datetime.now(),
locations=locations
)
# Check if hostname or IP already exists
if Server.query.filter_by(hostname=hostname).first():
flash("Hostname already exists", "danger")
# If no subnet is selected, location is required
if not subnet_id and not location_id:
flash("Either a subnet or a location is required", "danger")
return render_template(
"dashboard/server_form.html",
title="New Server",
subnets=subnets,
now=datetime.now(),
)
if Server.query.filter_by(ip_address=ip_address).first():
flash("IP address already exists", "danger")
return render_template(
"dashboard/server_form.html",
title="New Server",
subnets=subnets,
now=datetime.now(),
locations=locations
)
# Create new server
@ -119,88 +113,85 @@ def server_new():
hostname=hostname,
ip_address=ip_address,
subnet_id=subnet_id,
documentation=documentation,
location_id=location_id,
description=description,
user_id=current_user.id
)
db.session.add(server)
db.session.commit()
flash("Server created successfully", "success")
flash(f"Server {hostname} created successfully", "success")
return redirect(url_for("dashboard.server_view", server_id=server.id))
return render_template(
"dashboard/server_form.html",
title="New Server",
subnets=subnets,
now=datetime.now(),
locations=locations
)
@bp.route("/server/<int:server_id>/edit", methods=["GET", "POST"])
@login_required
def server_edit(server_id):
"""Edit an existing server"""
"""Edit a server"""
server = Server.query.get_or_404(server_id)
subnets = Subnet.query.all()
# Get all unique locations for datalist
subnet_locations = db.session.query(Subnet.location).distinct().all()
server_locations = db.session.query(Server.location).filter(Server.location != None).distinct().all()
locations = sorted(set([loc[0] for loc in subnet_locations + server_locations if loc[0]]))
if request.method == "POST":
hostname = request.form.get("hostname")
ip_address = request.form.get("ip_address")
subnet_id = request.form.get("subnet_id")
documentation = request.form.get("documentation", "")
subnet_id = request.form.get("subnet_id") or None
location = request.form.get("location")
if not hostname or not ip_address or not subnet_id:
flash("All fields are required", "danger")
# Validate inputs
if not hostname or not ip_address:
flash("Hostname and IP address are required", "danger")
return render_template(
"dashboard/server_form.html",
title="Edit Server",
server=server,
subnets=subnets,
locations=locations,
edit_mode=True
)
# Check if hostname changed and already exists
if (
hostname != server.hostname
and Server.query.filter_by(hostname=hostname).first()
):
flash("Hostname already exists", "danger")
return render_template(
"dashboard/server_form.html",
title="Edit Server",
server=server,
subnets=subnets,
)
# Check if IP changed and already exists
if (
ip_address != server.ip_address
and Server.query.filter_by(ip_address=ip_address).first()
):
flash("IP address already exists", "danger")
# If no subnet is selected, location is required
if not subnet_id and not location:
flash("Location is required for servers without a subnet", "danger")
return render_template(
"dashboard/server_form.html",
title="Edit Server",
server=server,
subnets=subnets,
locations=locations,
edit_mode=True
)
# Update server
server.hostname = hostname
server.ip_address = ip_address
server.subnet_id = subnet_id
server.documentation = documentation
server.location = location if not subnet_id else None
db.session.commit()
flash("Server updated successfully", "success")
flash(f"Server {hostname} updated successfully", "success")
return redirect(url_for("dashboard.server_view", server_id=server.id))
# GET request - show form with current values
return render_template(
"dashboard/server_form.html",
title=f"Edit Server - {server.hostname}",
title="Edit Server",
server=server,
subnets=subnets,
locations=locations,
edit_mode=True
)
@ -221,120 +212,79 @@ def server_delete(server_id):
return redirect(url_for("dashboard.dashboard_home"))
@bp.route("/app/new", methods=["GET", "POST"])
@bp.route("/app/new", defaults={'server_id': None}, methods=["GET", "POST"])
@bp.route("/app/new/<int:server_id>", methods=["GET", "POST"])
@login_required
def app_new(server_id=None):
"""Create a new application"""
servers = Server.query.all()
"""Create a new application for a server"""
# Get all servers for the dropdown
servers = Server.query.filter_by(user_id=current_user.id).all()
# If server_id is provided, validate it
selected_server = None
if server_id:
selected_server = Server.query.get_or_404(server_id)
if selected_server.user_id != current_user.id:
abort(403)
if request.method == "POST":
name = request.form.get("name", "").strip()
server_id = request.form.get("server_id")
documentation = request.form.get("documentation", "")
name = request.form.get("name")
url = request.form.get("url", "")
documentation = request.form.get("documentation", "")
form_server_id = request.form.get("server_id")
# Validate application name
if not name:
flash("Application name is required", "danger")
# Validate inputs
if not name or not form_server_id:
flash("Application name and server are required", "danger")
return render_template(
"dashboard/app_form.html",
edit_mode=False,
title="New Application",
servers=servers,
selected_server_id=server_id
selected_server_id=form_server_id or server_id,
edit_mode=False,
app={} # Empty app object for the template
)
# Check for duplicate application names on the same server
existing_app = App.query.filter_by(name=name, server_id=server_id).first()
if existing_app:
flash(f"An application with the name '{name}' already exists on this server", "danger")
return render_template(
"dashboard/app_form.html",
edit_mode=False,
servers=servers,
selected_server_id=server_id
)
# Verify the selected server belongs to the user
server = Server.query.get_or_404(form_server_id)
if server.user_id != current_user.id:
abort(403)
# Process port data from form
port_data = []
port_numbers = request.form.getlist("port_numbers[]")
protocols = request.form.getlist("protocols[]")
descriptions = request.form.getlist("port_descriptions[]")
for i in range(len(port_numbers)):
if port_numbers[i] and port_numbers[i].strip():
protocol = protocols[i] if i < len(protocols) else "TCP"
description = descriptions[i] if i < len(descriptions) else ""
port_data.append((port_numbers[i], protocol, description))
# Check for port conflicts proactively
conflicts = []
seen_ports = set() # To track ports already seen in this submission
for i, (port_number, protocol, _) in enumerate(port_data):
try:
clean_port = int(port_number)
# Check if this port has already been seen in this submission
port_key = f"{clean_port}/{protocol}"
if port_key in seen_ports:
conflicts.append((clean_port, protocol, "Duplicate port in submission"))
continue
seen_ports.add(port_key)
# Check if the port is in use by another application
in_use, conflicting_app_name = is_port_in_use(
clean_port, protocol, server_id
)
if in_use:
conflicts.append((clean_port, protocol, f"Port {clean_port}/{protocol} is already in use by application '{conflicting_app_name}'"))
except (ValueError, TypeError):
continue
if conflicts:
for conflict in conflicts:
flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger")
return render_template(
"dashboard/app_form.html",
edit_mode=False,
servers=servers,
selected_server_id=server_id
)
# Create the application
try:
new_app = App(
# Create new application
app = App(
name=name,
server_id=server_id,
documentation=documentation,
url=url
server_id=form_server_id,
user_id=current_user.id, # Set user_id explicitly
documentation=documentation or "",
url=url or ""
)
db.session.add(new_app)
db.session.flush() # Get the app ID without committing
# Add ports
for port_number, protocol, description in port_data:
new_port = Port(
app_id=new_app.id,
port_number=int(port_number),
protocol=protocol,
description=description
)
db.session.add(new_port)
db.session.add(app)
db.session.commit()
flash(f"Application '{name}' created successfully", "success")
return redirect(url_for("dashboard.app_view", app_id=new_app.id))
flash(f"Application {name} created successfully", "success")
return redirect(url_for("dashboard.server_view", server_id=form_server_id))
except Exception as e:
db.session.rollback()
flash(f"Error creating application: {str(e)}", "danger")
# GET request or validation failed - render the form
return render_template(
"dashboard/app_form.html",
edit_mode=False,
title="New Application",
servers=servers,
selected_server_id=server_id
selected_server_id=form_server_id or server_id,
edit_mode=False,
app={} # Empty app object for the template
)
# GET request - show the form
return render_template(
"dashboard/app_form.html",
title="New Application",
servers=servers,
selected_server_id=server_id,
edit_mode=False,
app={} # Empty app object for the template
)
@ -357,118 +307,71 @@ def app_view(app_id):
@bp.route("/app/<int:app_id>/edit", methods=["GET", "POST"])
@login_required
def app_edit(app_id):
"""Edit an existing application with comprehensive error handling"""
"""Edit an existing application"""
app = App.query.get_or_404(app_id)
servers = Server.query.all()
# Check that the current user owns this app
if app.user_id != current_user.id:
abort(403)
# Get all servers for the dropdown
servers = Server.query.filter_by(user_id=current_user.id).all()
if request.method == "POST":
# Get form data
name = request.form.get("name", "").strip()
server_id = request.form.get("server_id")
name = request.form.get("name")
url = request.form.get("url", "")
documentation = request.form.get("documentation", "")
url = request.form.get("url", "") # Get the URL
server_id = request.form.get("server_id")
port_data = request.form.getlist("port")
port_descriptions = request.form.getlist("port_description")
# Validate application name
if not name:
flash("Application name is required", "danger")
return render_template(
"dashboard/app_form.html",
edit_mode=True,
app=app,
servers=servers
)
# Validate required fields
if not name or not server_id:
flash("Application name and server are required", "danger")
return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True)
# Check for duplicate application names on the same server (excluding this app)
existing_app = App.query.filter(
App.name == name,
App.server_id == server_id,
App.id != app_id
).first()
# Validate port data - exclude current app's ports from conflict check
port_error = validate_port_data(port_data, port_descriptions, server_id, app_id)
if port_error:
flash(port_error, "danger")
return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True)
if existing_app:
flash(f"An application with the name '{name}' already exists on this server", "danger")
return render_template(
"dashboard/app_form.html",
edit_mode=True,
app=app,
servers=servers
)
# Process port data from form
port_data = []
port_numbers = request.form.getlist("port_numbers[]")
protocols = request.form.getlist("protocols[]")
descriptions = request.form.getlist("port_descriptions[]")
for i in range(len(port_numbers)):
if port_numbers[i] and port_numbers[i].strip():
protocol = protocols[i] if i < len(protocols) else "TCP"
description = descriptions[i] if i < len(descriptions) else ""
port_data.append((port_numbers[i], protocol, description))
# Check for port conflicts proactively
conflicts = []
seen_ports = set() # To track ports already seen in this submission
for i, (port_number, protocol, _) in enumerate(port_data):
try:
clean_port = int(port_number)
# Check if this port has already been seen in this submission
port_key = f"{clean_port}/{protocol}"
if port_key in seen_ports:
conflicts.append((clean_port, protocol, "Duplicate port in submission"))
continue
seen_ports.add(port_key)
# Check if the port is in use by another application
in_use, conflicting_app_name = is_port_in_use(
clean_port, protocol, server_id, exclude_app_id=app_id
)
if in_use:
conflicts.append((clean_port, protocol, f"Port {clean_port}/{protocol} is already in use by application '{conflicting_app_name}'"))
except (ValueError, TypeError):
continue
if conflicts:
for conflict in conflicts:
flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger")
return render_template(
"dashboard/app_form.html",
edit_mode=True,
app=app,
servers=servers
)
# Update application details
# Update app details
app.name = name
app.server_id = server_id
app.documentation = documentation
app.url = url
app.updated_at = datetime.utcnow()
# Only delete existing ports if new port data is provided
if port_data:
# Remove existing ports and add new ones
Port.query.filter_by(app_id=app_id).delete()
for port_number, protocol, description in port_data:
new_port = Port(
app_id=app_id,
port_number=int(port_number),
protocol=protocol,
description=description
# Update ports
# First, remove all existing ports
Port.query.filter_by(app_id=app.id).delete()
# Then add the new ports
for i, port_number in enumerate(port_data):
if not port_number: # Skip empty port entries
continue
description = port_descriptions[i] if i < len(port_descriptions) else ""
port = Port(
port_number=port_number,
description=description,
app_id=app.id
)
db.session.add(new_port)
db.session.add(port)
db.session.commit()
flash("Application updated successfully", "success")
return redirect(url_for("dashboard.app_view", app_id=app_id))
flash(f"Application {name} updated successfully", "success")
return redirect(url_for("dashboard.server_view", server_id=server_id))
return render_template(
"dashboard/app_form.html",
edit_mode=True,
app=app,
servers=servers
)
except Exception as e:
db.session.rollback()
flash(f"Error updating application: {str(e)}", "danger")
return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True)
# GET request - show the form
return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True)
@bp.route("/app/<int:app_id>/delete", methods=["POST"])
@ -534,73 +437,73 @@ def settings():
@bp.route("/overview")
@login_required
def overview():
"""Hierarchical overview of subnets, servers, and applications"""
# Get all subnets with their servers
subnets = Subnet.query.all()
"""Display an overview of the infrastructure"""
# Get all servers, subnets, apps for the current user
servers = Server.query.filter_by(user_id=current_user.id).all()
subnets = Subnet.query.filter_by(user_id=current_user.id).all()
locations = Location.query.filter_by(user_id=current_user.id).all()
# Get servers without subnets
standalone_servers = Server.query.filter(Server.subnet_id.is_(None)).all()
# Create location lookup dictionary for quick access
location_lookup = {loc.id: loc.name for loc in locations}
# Create a hierarchical structure
hierarchy = {
'subnets': [],
'standalone_servers': []
}
# Count servers by subnet
servers_by_subnet = {}
unassigned_servers = []
# Organize subnets and their servers
# Process servers
for server in servers:
if server.subnet_id:
if server.subnet_id not in servers_by_subnet:
servers_by_subnet[server.subnet_id] = []
servers_by_subnet[server.subnet_id].append(server)
else:
unassigned_servers.append(server)
# Prepare subnet data for the chart
subnet_data = []
for subnet in subnets:
subnet_data = {
# Get the location name using the location_id
location_name = location_lookup.get(subnet.location_id, 'Unassigned')
subnet_data.append({
'id': subnet.id,
'cidr': subnet.cidr,
'location': subnet.location,
'servers': []
}
'location': location_name,
'server_count': len(servers_by_subnet.get(subnet.id, [])),
'servers': servers_by_subnet.get(subnet.id, [])
})
# Only add description if it exists as an attribute
if hasattr(subnet, 'description'):
subnet_data['description'] = subnet.description
for server in subnet.servers:
server_data = {
# Count apps by server
servers_with_app_count = []
for server in servers:
app_count = len(server.apps) # Adjust this if needed based on your relationship
servers_with_app_count.append({
'id': server.id,
'hostname': server.hostname,
'ip_address': server.ip_address,
'apps': []
}
'app_count': app_count
})
for app in server.apps:
app_data = {
'id': app.id,
'name': app.name,
'ports': app.ports
}
server_data['apps'].append(app_data)
subnet_data['servers'].append(server_data)
hierarchy['subnets'].append(subnet_data)
# Organize standalone servers
for server in standalone_servers:
server_data = {
'id': server.id,
'hostname': server.hostname,
'ip_address': server.ip_address,
'apps': []
}
for app in server.apps:
app_data = {
'id': app.id,
'name': app.name,
'ports': app.ports
}
server_data['apps'].append(app_data)
hierarchy['standalone_servers'].append(server_data)
# Sort by app count descending
servers_with_app_count.sort(key=lambda x: x['app_count'], reverse=True)
return render_template(
"dashboard/overview.html",
title="Infrastructure Overview",
hierarchy=hierarchy
subnet_count=len(subnets),
server_count=len(servers),
subnet_data=subnet_data,
unassigned_servers=unassigned_servers,
servers_with_app_count=servers_with_app_count[:5] # Top 5 servers
)
@bp.route("/apps")
@login_required
def app_list():
"""List all applications"""
apps = App.query.order_by(App.name).all()
return render_template(
"dashboard/app_list.html",
title="Applications",
apps=apps
)

View file

@ -1,6 +1,6 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify
from flask_login import login_required
from app.core.models import Subnet, Server, App
from flask_login import login_required, current_user
from app.core.models import Subnet, Server, App, Location
from app.core.extensions import db
from app.scripts.ip_scanner import scan
import ipaddress
@ -36,32 +36,24 @@ def ipam_home():
@login_required
def subnet_new():
"""Create a new subnet"""
# Get all locations for the dropdown
locations = Location.query.filter_by(user_id=current_user.id).all()
if request.method == "POST":
cidr = request.form.get("cidr")
location = request.form.get("location")
location_id = request.form.get("location_id")
auto_scan = request.form.get("auto_scan") == "on"
# Basic validation
if not cidr or not location:
flash("Please fill in all required fields", "danger")
return render_template("ipam/subnet_form.html", title="New Subnet")
if not cidr or not location_id:
flash("CIDR notation and location are required", "danger")
return render_template("ipam/subnet_form.html", title="New Subnet", locations=locations)
# Validate CIDR format
try:
ipaddress.ip_network(cidr, strict=False)
except ValueError:
flash("Invalid CIDR format", "danger")
return render_template("ipam/subnet_form.html", title="New Subnet")
# Check if CIDR already exists
if Subnet.query.filter_by(cidr=cidr).first():
flash("Subnet already exists", "danger")
return render_template("ipam/subnet_form.html", title="New Subnet")
# Create new subnet with JSON string for active_hosts, not a Python list
# Create new subnet
subnet = Subnet(
cidr=cidr,
location=location,
location_id=location_id,
user_id=current_user.id,
active_hosts=json.dumps([]), # Convert empty list to JSON string
last_scanned=None,
auto_scan=auto_scan,
@ -73,7 +65,7 @@ def subnet_new():
flash("Subnet created successfully", "success")
return redirect(url_for("ipam.subnet_view", subnet_id=subnet.id))
return render_template("ipam/subnet_form.html", title="New Subnet")
return render_template("ipam/subnet_form.html", title="New Subnet", locations=locations)
@bp.route("/subnet/<int:subnet_id>")
@ -267,43 +259,39 @@ def subnet_create_ajax():
return jsonify({"success": False, "error": str(e)})
@bp.route("/location/<location>")
@bp.route("/location/<int:location_id>")
@login_required
def location_overview(location):
def location_overview(location_id):
"""View all subnets and servers in a specific location"""
# Get all subnets in this location
subnets = Subnet.query.filter_by(location=location).all()
# Get servers in these subnets
servers = []
for subnet in subnets:
subnet_servers = Server.query.filter_by(subnet_id=subnet.id).all()
servers.extend(subnet_servers)
# Get the location (ensure it belongs to the current user)
location = Location.query.filter_by(id=location_id, user_id=current_user.id).first_or_404()
# Create a hierarchical structure
hierarchy = {
'locations': {
location.name: {
'id': location.id,
'description': location.description,
'subnets': [],
'standalone_servers': []
}
}
}
# Organize subnets and their servers
for subnet in subnets:
for subnet in location.subnets:
subnet_data = {
'id': subnet.id,
'cidr': subnet.cidr,
'location': subnet.location,
'servers': []
}
# Only add description if it exists as an attribute
if hasattr(subnet, 'description'):
subnet_data['description'] = subnet.description
for server in subnet.servers:
server_data = {
'id': server.id,
'hostname': server.hostname,
'ip_address': server.ip_address,
'description': server.description,
'apps': []
}
@ -311,17 +299,59 @@ def location_overview(location):
app_data = {
'id': app.id,
'name': app.name,
'ports': app.ports
'url': app.url,
'ports': []
}
for port in app.ports:
port_data = {
'id': port.id,
'number': port.port_number,
'protocol': port.protocol,
'description': port.description
}
app_data['ports'].append(port_data)
server_data['apps'].append(app_data)
subnet_data['servers'].append(server_data)
hierarchy['subnets'].append(subnet_data)
hierarchy['locations'][location.name]['subnets'].append(subnet_data)
# Add standalone servers
for server in location.standalone_servers:
server_data = {
'id': server.id,
'hostname': server.hostname,
'ip_address': server.ip_address,
'description': server.description,
'apps': []
}
for app in server.apps:
app_data = {
'id': app.id,
'name': app.name,
'url': app.url,
'ports': []
}
for port in app.ports:
port_data = {
'id': port.id,
'number': port.port_number,
'protocol': port.protocol,
'description': port.description
}
app_data['ports'].append(port_data)
server_data['apps'].append(app_data)
hierarchy['locations'][location.name]['standalone_servers'].append(server_data)
return render_template(
"dashboard/overview.html",
title=f"{location} Overview",
"ipam/location_overview.html",
title=f"{location.name} Overview",
hierarchy=hierarchy,
location=location
)

View file

@ -0,0 +1,56 @@
// API Functions for reuse across the application
const apiFunctions = {
// Create a new location
createLocation: function (name, description, csrfToken) {
return fetch('/api/locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
name: name,
description: description
})
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to create location');
}
return response.json();
});
},
// Create a new subnet
createSubnet: function (cidr, locationId, autoScan, csrfToken) {
return fetch('/api/subnets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
cidr: cidr,
location_id: locationId,
auto_scan: autoScan
})
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to create subnet');
}
return response.json();
});
},
// Get all subnets
getSubnets: function () {
return fetch('/api/subnets')
.then(response => {
if (!response.ok) {
throw new Error('Failed to load subnets');
}
return response.json();
});
}
};

53
app/static/js/sidebar.js Normal file
View file

@ -0,0 +1,53 @@
// Function to load subnets in the sidebar
function loadSubnets() {
fetch('/api/subnets')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// Update the sidebar with the fetched subnets
const subnetList = document.getElementById('subnet-list');
subnetList.innerHTML = ''; // Clear existing items
if (data.length === 0) {
subnetList.innerHTML = '<div class="nav-item"><span class="nav-link">No subnets found</span></div>';
return;
}
data.forEach(site => {
// Create site header if it has subnets
if (site.subnets && site.subnets.length > 0) {
const siteHeader = document.createElement('div');
siteHeader.className = 'nav-item';
siteHeader.innerHTML = `<span class="nav-link text-muted">${site.name || 'Unassigned'}</span>`;
subnetList.appendChild(siteHeader);
// Add each subnet under this site
site.subnets.forEach(subnet => {
const subnetItem = document.createElement('div');
subnetItem.className = 'nav-item';
subnetItem.innerHTML = `
<a href="/ipam/subnet/${subnet.id}" class="nav-link">
<span class="nav-link-icon">
<i class="ti ti-network"></i>
</span>
<span>${subnet.cidr}</span>
</a>
`;
subnetList.appendChild(subnetItem);
});
}
});
})
.catch(error => {
console.error('Error loading subnets:', error);
const subnetList = document.getElementById('subnet-list');
subnetList.innerHTML = '<div class="nav-item text-danger">Error loading subnets</div>';
});
}
// Call this function when the page loads
document.addEventListener('DOMContentLoaded', loadSubnets);

View file

@ -0,0 +1,97 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-xl">
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
Applications
</h2>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<a href="{{ url_for('dashboard.app_new') }}" class="btn btn-primary d-none d-sm-inline-block">
<span class="ti ti-plus me-2"></span>
New Application
</a>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">All Applications</h3>
</div>
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>Name</th>
<th>Server</th>
<th>Ports</th>
<th>Created</th>
<th class="w-1"></th>
</tr>
</thead>
<tbody>
{% for app in apps %}
<tr>
<td>
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}">{{ app.name }}</a>
</td>
<td>
<a href="{{ url_for('dashboard.server_view', server_id=app.server.id) }}">
{{ app.server.hostname }}
</a>
</td>
<td>
{% if app.ports %}
{% for port in app.ports %}
<span class="badge bg-azure me-1">{{ port.port_number }}/{{ port.protocol }}</span>
{% endfor %}
{% else %}
<span class="text-muted">No ports defined</span>
{% endif %}
</td>
<td class="text-muted">{{ app.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="btn-list flex-nowrap">
<a href="{{ url_for('dashboard.app_edit', app_id=app.id) }}" class="btn btn-sm btn-outline-primary">
Edit
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-4">
<div class="empty">
<div class="empty-img">
<span class="ti ti-apps" style="font-size: 3rem;"></span>
</div>
<p class="empty-title">No applications found</p>
<p class="empty-subtitle text-muted">
Start by creating a new application.
</p>
<div class="empty-action">
<a href="{{ url_for('dashboard.app_new') }}" class="btn btn-primary">
<span class="ti ti-plus me-2"></span>
New Application
</a>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -6,7 +6,7 @@
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
Infrastructure Overview
{{ title }}
</h2>
</div>
<div class="col-auto ms-auto d-print-none">
@ -19,16 +19,11 @@
<span class="ti ti-plus me-2"></span>
New Server
</a>
<a href="{{ url_for('dashboard.app_new') }}" class="btn btn-primary d-none d-sm-inline-block">
<span class="ti ti-plus me-2"></span>
New Application
</a>
</div>
</div>
</div>
</div>
<!-- Hierarchical View -->
<div class="row mt-3">
<div class="col-12">
<div class="card">
@ -36,9 +31,19 @@
<h3 class="card-title">Network Infrastructure</h3>
</div>
<div class="card-body">
<!-- Subnets -->
{% if hierarchy.subnets %}
{% for subnet in hierarchy.subnets %}
{% if hierarchy.locations %}
{% for location_name, location_data in hierarchy.locations.items() %}
<div class="location-container mb-5">
<div class="location-header d-flex align-items-center p-2 bg-primary-lt rounded">
<span class="ti ti-building me-2"></span>
<h3 class="m-0">{{ location_name }}</h3>
</div>
<!-- Subnets in this location -->
{% if location_data.subnets %}
<div class="ms-4 mt-3">
<h4 class="mb-3">Subnets</h4>
{% for subnet in location_data.subnets %}
<div class="subnet-container mb-4">
<div class="subnet-header d-flex align-items-center p-2 bg-azure-lt rounded">
<span class="ti ti-network me-2"></span>
@ -46,9 +51,6 @@
<a href="{{ url_for('ipam.subnet_view', subnet_id=subnet.id) }}" class="text-reset">
{{ subnet.cidr }}
</a>
{% if subnet.location %}
<small class="text-muted ms-2">({{ subnet.location }})</small>
{% endif %}
{% if subnet.description is defined and subnet.description %}
<small class="text-muted ms-2">{{ subnet.description }}</small>
{% endif %}
@ -63,10 +65,10 @@
<!-- Servers in this subnet -->
{% if subnet.servers %}
<div class="ps-4 mt-2">
<div class="ms-4 mt-2">
{% for server in subnet.servers %}
<div class="server-container mb-3 border-start ps-3">
<div class="server-header d-flex align-items-center p-2 bg-light rounded">
<div class="server-container mb-3">
<div class="server-header d-flex align-items-center p-2 bg-light rounded border-start border-3">
<span class="ti ti-server me-2"></span>
<h5 class="m-0">
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}" class="text-reset">
@ -76,7 +78,7 @@
</h5>
<div class="ms-auto">
<a href="{{ url_for('dashboard.app_new') }}?server_id={{ server.id }}"
class="btn btn-sm btn-outline-primary">
class="btn btn-sm btn-outline-secondary">
<span class="ti ti-plus me-1"></span> Add App
</a>
</div>
@ -84,64 +86,60 @@
<!-- Apps on this server -->
{% if server.apps %}
<div class="ps-4 mt-2">
<div class="ms-4 mt-1">
{% for app in server.apps %}
<div class="app-container mb-2 border-start ps-3">
<div class="app-header d-flex align-items-center p-2 bg-light-lt rounded">
<div class="app-container mb-2">
<div class="app-header d-flex align-items-center p-2 bg-white rounded border-start border-3">
<span class="ti ti-app-window me-2"></span>
<h6 class="m-0">
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="text-reset">
{{ app.name }}
</a>
</h6>
<div class="ms-2">
{% if app.ports %}
<small class="text-muted ms-2">
Ports:
{% for port in app.ports %}
<span class="badge bg-blue-lt">{{ port.port_number }}/{{ port.protocol }}</span>
<span class="badge bg-blue-lt">{{ port.port }}/{{ port.protocol }}</span>
{% endfor %}
</div>
</small>
{% endif %}
</h6>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="ps-4 mt-2">
<div class="text-muted fst-italic">No applications</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="ps-4 mt-2">
<div class="text-muted fst-italic">No servers in this subnet</div>
<div class="ms-4 mt-2 text-muted">
<em>No servers in this subnet</em>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Standalone Servers -->
{% if hierarchy.standalone_servers %}
<div class="standalone-servers-container mb-4">
<div class="subnet-header d-flex align-items-center p-2 bg-yellow-lt rounded">
<span class="ti ti-server me-2"></span>
<h4 class="m-0">Standalone Servers</h4>
</div>
<div class="ps-4 mt-2">
{% for server in hierarchy.standalone_servers %}
<div class="server-container mb-3 border-start ps-3">
<div class="server-header d-flex align-items-center p-2 bg-light rounded">
<!-- Standalone servers in this location -->
{% if location_data.standalone_servers %}
<div class="ms-4 mt-3">
<h4 class="mb-3">Standalone Servers</h4>
{% for server in location_data.standalone_servers %}
<div class="server-container mb-3">
<div class="server-header d-flex align-items-center p-2 bg-light rounded border-start border-3">
<span class="ti ti-server me-2"></span>
<h5 class="m-0">
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}" class="text-reset">
{{ server.hostname }}
</a>
<small class="text-muted ms-2">{{ server.ip_address }}</small>
<span class="badge bg-purple-lt ms-2">Public IP</span>
</h5>
<div class="ms-auto">
<a href="{{ url_for('dashboard.app_new') }}?server_id={{ server.id }}"
class="btn btn-sm btn-outline-primary">
class="btn btn-sm btn-outline-secondary">
<span class="ti ti-plus me-1"></span> Add App
</a>
</div>
@ -149,37 +147,36 @@
<!-- Apps on this server -->
{% if server.apps %}
<div class="ps-4 mt-2">
<div class="ms-4 mt-1">
{% for app in server.apps %}
<div class="app-container mb-2 border-start ps-3">
<div class="app-header d-flex align-items-center p-2 bg-light-lt rounded">
<div class="app-container mb-2">
<div class="app-header d-flex align-items-center p-2 bg-white rounded border-start border-3">
<span class="ti ti-app-window me-2"></span>
<h6 class="m-0">
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="text-reset">
{{ app.name }}
</a>
</h6>
<div class="ms-2">
{% if app.ports %}
<small class="text-muted ms-2">
Ports:
{% for port in app.ports %}
<span class="badge bg-blue-lt">{{ port.port_number }}/{{ port.protocol }}</span>
<span class="badge bg-blue-lt">{{ port.port }}/{{ port.protocol }}</span>
{% endfor %}
</div>
</small>
{% endif %}
</h6>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="ps-4 mt-2">
<div class="text-muted fst-italic">No applications</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if not hierarchy.subnets and not hierarchy.standalone_servers %}
<div class="text-center py-4">
<div class="empty">
<div class="empty-img">
@ -209,6 +206,7 @@
</div>
<style>
.location-container,
.subnet-container,
.server-container,
.app-container {
@ -224,12 +222,14 @@
border-left-color: var(--tblr-primary) !important;
}
.location-header,
.subnet-header,
.server-header,
.app-header {
transition: all 0.2s ease;
}
.location-header:hover,
.subnet-header:hover,
.server-header:hover,
.app-header:hover {

View file

@ -6,163 +6,285 @@
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
{% if server %}Edit Server{% else %}Add New Server{% endif %}
{{ title }}
</h2>
</div>
</div>
</div>
<div class="card mt-3">
<div class="row mt-3">
<div class="col-12">
<div class="card">
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST"
action="{% if server %}{{ url_for('dashboard.server_edit', server_id=server.id) }}{% else %}{{ url_for('dashboard.server_new') }}{% endif %}">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label required">Hostname</label>
<input type="text" class="form-control" name="hostname" required
value="{% if server %}{{ server.hostname }}{% endif %}">
<label for="hostname" class="form-label">Hostname</label>
<input type="text" class="form-control" id="hostname" name="hostname" required
value="{{ server.hostname if server else '' }}">
</div>
<div class="mb-3">
<label class="form-label required">IP Address</label>
<input type="text" class="form-control" name="ip_address" placeholder="192.168.1.10" required
value="{% if server %}{{ server.ip_address }}{% endif %}">
<label for="ip_address" class="form-label">IP Address</label>
<input type="text" class="form-control" id="ip_address" name="ip_address" required
value="{{ server.ip_address if server else '' }}">
</div>
<div class="mb-3">
<label class="form-label required">Subnet</label>
<div class="input-group">
<select class="form-select" name="subnet_id" required id="subnet-select">
<option value="">Select a subnet</option>
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<label for="subnet_id" class="form-label">Subnet (optional for public IPs)</label>
<select class="form-select" id="subnet_id" name="subnet_id">
<option value="">No subnet (standalone server)</option>
{% for subnet in subnets %}
<option value="{{ subnet.id }}" {% if server and server.subnet_id==subnet.id %}selected{% endif %}>
{{ subnet.cidr }} ({{ subnet.location }})
{{ subnet.cidr }} ({{ subnet.location_ref.name }})
</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal"
data-bs-target="#quickSubnetModal">
</div>
<div class="ms-2 pt-4">
<button type="button" class="btn btn-outline-primary btn-icon" data-bs-toggle="modal"
data-bs-target="#add-subnet-modal">
<span class="ti ti-plus"></span>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Documentation</label>
<textarea class="form-control" name="documentation"
rows="10">{% if server %}{{ server.documentation }}{% endif %}</textarea>
<div class="form-text">Markdown is supported</div>
</div>
<div class="mb-3">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<label for="location_id" class="form-label">Location (required for standalone servers)</label>
<select class="form-select" id="location_id" name="location_id">
<option value="">Select a location</option>
{% for location in locations %}
<option value="{{ location.id }}" {% if server and server.location_id==location.id %}selected{%
endif %}>
{{ location.name }}
</option>
{% endfor %}
</select>
</div>
<div class="ms-2 pt-4">
<button type="button" class="btn btn-outline-primary btn-icon" data-bs-toggle="modal"
data-bs-target="#add-location-modal">
<span class="ti ti-plus"></span>
</button>
</div>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description (optional)</label>
<textarea class="form-control" id="description" name="description"
rows="3">{{ server.description if server else '' }}</textarea>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">Save</button>
{% if server %}
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}"
class="btn btn-outline-secondary ms-2">Cancel</a>
{% else %}
<a href="{{ url_for('dashboard.dashboard_home') }}" class="btn btn-outline-secondary ms-2">Cancel</a>
{% endif %}
<a href="{{ url_for('dashboard.server_list') }}" class="btn btn-link">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Subnet Creation Modal -->
<div class="modal fade" id="quickSubnetModal" tabindex="-1" aria-labelledby="quickSubnetModalLabel" aria-hidden="true">
<div class="modal-dialog">
<!-- Add Subnet Modal -->
<div class="modal modal-blur fade" id="add-subnet-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="quickSubnetModalLabel">Quick Subnet Creation</h5>
<h5 class="modal-title">Add New Subnet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="quickSubnetForm">
<div class="mb-3">
<form id="add-subnet-form">
<div class="row mb-3">
<div class="col-md-8">
<label class="form-label required">IP Address</label>
<input type="text" class="form-control" id="subnet-ip" placeholder="192.168.1.0" required>
<input type="text" class="form-control" id="new-subnet-ip" placeholder="192.168.1.0" required>
</div>
<div class="mb-3">
<div class="col-md-4">
<label class="form-label required">Prefix</label>
<select class="form-select" id="subnet-prefix" required>
<select class="form-select" id="new-subnet-prefix" required>
{% for i in range(8, 31) %}
<option value="{{ i }}" {% if i==24 %}selected{% endif %}>{{ i }}</option>
<option value="{{ i }}">{{ i }} ({{ 2**(32-i) }} hosts)</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label required">Location</label>
<input type="text" class="form-control" id="subnet-location" required>
<select class="form-select" id="new-subnet-location" required>
<option value="">Select a location</option>
{% for location in locations %}
<option value="{{ location.id }}">{{ location.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="new-subnet-auto-scan">
<label class="form-check-label" for="new-subnet-auto-scan">
Auto-scan for active hosts
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="createSubnetBtn">Create</button>
<button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn btn-primary ms-auto" id="save-subnet-btn">
<span class="ti ti-plus me-2"></span>
Add Subnet
</button>
</div>
</div>
</div>
</div>
<!-- Add Location Modal -->
<div class="modal modal-blur fade" id="add-location-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Location</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="add-location-form">
<div class="mb-3">
<label class="form-label required">Name</label>
<input type="text" class="form-control" id="new-location-name" required>
</div>
<div class="mb-3">
<label class="form-label">Description (optional)</label>
<textarea class="form-control" id="new-location-description" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn btn-primary ms-auto" id="save-location-btn">
<span class="ti ti-plus me-2"></span>
Add Location
</button>
</div>
</div>
</div>
</div>
<!-- Add this JavaScript at the end -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const createSubnetBtn = document.getElementById('createSubnetBtn');
const subnetSelect = document.getElementById('subnet_id');
const locationField = document.querySelector('.mb-3:has(#location_id)');
createSubnetBtn.addEventListener('click', function () {
const ip = document.getElementById('subnet-ip').value;
const prefix = document.getElementById('subnet-prefix').value;
const location = document.getElementById('subnet-location').value;
function updateLocationVisibility() {
if (subnetSelect.value === '') {
locationField.style.display = 'block';
document.getElementById('location_id').setAttribute('required', 'required');
} else {
locationField.style.display = 'none';
document.getElementById('location_id').removeAttribute('required');
}
}
// Validate inputs
if (!ip || !prefix || !location) {
// Initial state
if (locationField) {
updateLocationVisibility();
// Update on change
subnetSelect.addEventListener('change', updateLocationVisibility);
}
// Add subnet functionality
const saveSubnetBtn = document.getElementById('save-subnet-btn');
const addSubnetModal = document.getElementById('add-subnet-modal');
if (saveSubnetBtn && addSubnetModal) {
const bsSubnetModal = new bootstrap.Modal(addSubnetModal);
saveSubnetBtn.addEventListener('click', function () {
const ip = document.getElementById('new-subnet-ip').value.trim();
const prefix = document.getElementById('new-subnet-prefix').value;
const locationId = document.getElementById('new-subnet-location').value;
const autoScan = document.getElementById('new-subnet-auto-scan').checked;
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!ip || !prefix || !locationId) {
alert('All fields are required');
return;
}
// Create the subnet via AJAX
fetch('/ipam/subnet/create-ajax', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value
},
body: JSON.stringify({
cidr: `${ip}/${prefix}`,
location: location,
auto_scan: false
})
})
.then(response => response.json())
apiFunctions.createSubnet(`${ip}/${prefix}`, locationId, autoScan, csrfToken)
.then(data => {
if (data.success) {
// Add the new subnet to the dropdown
const selectElement = document.getElementById('subnet-select');
const option = document.createElement('option');
option.value = data.subnet_id;
option.text = `${data.cidr} (${data.location})`;
option.selected = true;
selectElement.appendChild(option);
// Add new option to select dropdown
const locationName = document.querySelector(`#new-subnet-location option[value="${locationId}"]`).textContent;
const newOption = new Option(`${data.cidr} (${locationName})`, data.id, true, true);
subnetSelect.add(newOption);
// Close the modal
const modal = bootstrap.Modal.getInstance(document.getElementById('quickSubnetModal'));
modal.hide();
} else {
alert(data.error || 'Failed to create subnet');
}
// Reset form and close modal
document.getElementById('new-subnet-ip').value = '';
document.getElementById('new-subnet-prefix').value = '24';
document.getElementById('new-subnet-location').value = '';
document.getElementById('new-subnet-auto-scan').checked = false;
bsSubnetModal.hide();
// Trigger the subnet change event to hide location if needed
const event = new Event('change');
subnetSelect.dispatchEvent(event);
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
alert('Failed to create subnet: ' + error.message);
});
});
}
// Add location functionality
const saveLocationBtn = document.getElementById('save-location-btn');
const locationSelect = document.getElementById('location_id');
const addLocationModal = document.getElementById('add-location-modal');
if (saveLocationBtn && locationSelect && addLocationModal) {
const bsLocationModal = new bootstrap.Modal(addLocationModal);
saveLocationBtn.addEventListener('click', function () {
const name = document.getElementById('new-location-name').value.trim();
const description = document.getElementById('new-location-description').value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!name) {
alert('Location name is required');
return;
}
apiFunctions.createLocation(name, description, csrfToken)
.then(data => {
// Add new option to select dropdown
const newOption = new Option(data.name, data.id, true, true);
locationSelect.add(newOption);
// Reset form and close modal
document.getElementById('new-location-name').value = '';
document.getElementById('new-location-description').value = '';
bsLocationModal.hide();
})
.catch(error => {
console.error('Error:', error);
alert('Failed to create location: ' + error.message);
});
});
}
});
</script>
{% endblock %}

View file

@ -52,9 +52,26 @@
<input type="hidden" name="cidr" id="cidr-value" value="{% if subnet %}{{ subnet.cidr }}{% endif %}">
<div class="mb-3">
<label class="form-label required">Location</label>
<input type="text" class="form-control" name="location" required
value="{% if subnet %}{{ subnet.location }}{% endif %}">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<label for="location_id" class="form-label required">Location</label>
<select class="form-select" id="location_id" name="location_id" required>
<option value="">Select a location</option>
{% for location in locations %}
<option value="{{ location.id }}" {% if subnet and subnet.location_id==location.id %}selected{% endif
%}>
{{ location.name }}
</option>
{% endfor %}
</select>
</div>
<div class="ms-2 pt-4">
<button type="button" class="btn btn-outline-primary btn-icon" data-bs-toggle="modal"
data-bs-target="#add-location-modal">
<span class="ti ti-plus"></span>
</button>
</div>
</div>
</div>
<div class="mb-3">
@ -77,48 +94,154 @@
</div>
</div>
<!-- Add Location Modal -->
<div class="modal modal-blur fade" id="add-location-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Location</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="add-location-form">
<div class="mb-3">
<label class="form-label required">Location Name</label>
<input type="text" class="form-control" id="new-location-name" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" id="new-location-description" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn btn-primary ms-auto" id="save-location-btn">
<span class="ti ti-plus me-2"></span>
Add Location
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Handle CIDR combination
const ipAddressInput = document.getElementById('ip-address');
const prefixSelect = document.getElementById('prefix');
const cidrValueInput = document.getElementById('cidr-value');
const cidrValue = document.getElementById('cidr-value');
// Function to update the hidden CIDR field
function updateCidrValue() {
function updateCIDR() {
const ip = ipAddressInput.value.trim();
const prefix = prefixSelect.value;
if (ip) {
cidrValueInput.value = `${ip}/${prefix}`;
cidrValue.value = `${ip}/${prefix}`;
}
}
// Add event listeners
ipAddressInput.addEventListener('input', updateCidrValue);
prefixSelect.addEventListener('change', updateCidrValue);
// Initialize CIDR value if editing
if (cidrValueInput.value) {
const parts = cidrValueInput.value.split('/');
if (parts.length === 2) {
ipAddressInput.value = parts[0];
const prefix = parseInt(parts[1]);
if (!isNaN(prefix) && prefix >= 8 && prefix <= 30) {
prefixSelect.value = prefix;
}
}
if (ipAddressInput && prefixSelect && cidrValue) {
ipAddressInput.addEventListener('input', updateCIDR);
prefixSelect.addEventListener('change', updateCIDR);
}
// Ensure form submission updates the CIDR value
document.querySelector('form').addEventListener('submit', function (e) {
updateCidrValue();
// Add location functionality
const saveLocationBtn = document.getElementById('save-location-btn');
const locationSelect = document.getElementById('location_id');
const addLocationModal = document.getElementById('add-location-modal');
// Basic validation
if (!cidrValueInput.value) {
e.preventDefault();
alert('Please enter a valid IP address and prefix');
if (saveLocationBtn && locationSelect && addLocationModal) {
const bsModal = new bootstrap.Modal(addLocationModal);
saveLocationBtn.addEventListener('click', function () {
const name = document.getElementById('new-location-name').value.trim();
const description = document.getElementById('new-location-description').value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!name) {
alert('Location name is required');
return;
}
apiFunctions.createLocation(name, description, csrfToken)
.then(data => {
// Add new option to select dropdown
const newOption = new Option(data.name, data.id, true, true);
locationSelect.add(newOption);
// Reset form and close modal
document.getElementById('new-location-name').value = '';
document.getElementById('new-location-description').value = '';
bsModal.hide();
})
.catch(error => {
console.error('Error:', error);
alert('Failed to create location: ' + error.message);
});
});
}
});
</script>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Handle CIDR combination
const ipAddressInput = document.getElementById('ip-address');
const prefixSelect = document.getElementById('prefix');
const cidrValue = document.getElementById('cidr-value');
function updateCIDR() {
const ip = ipAddressInput.value.trim();
const prefix = prefixSelect.value;
if (ip) {
cidrValue.value = `${ip}/${prefix}`;
}
}
if (ipAddressInput && prefixSelect && cidrValue) {
ipAddressInput.addEventListener('input', updateCIDR);
prefixSelect.addEventListener('change', updateCIDR);
}
// Add location functionality
const saveLocationBtn = document.getElementById('save-location-btn');
const locationSelect = document.getElementById('location_id');
const addLocationModal = document.getElementById('add-location-modal');
if (saveLocationBtn && locationSelect && addLocationModal) {
const bsModal = new bootstrap.Modal(addLocationModal);
saveLocationBtn.addEventListener('click', function () {
const name = document.getElementById('new-location-name').value.trim();
const description = document.getElementById('new-location-description').value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!name) {
alert('Location name is required');
return;
}
apiFunctions.createLocation(name, description, csrfToken)
.then(data => {
// Add new option to select dropdown
const newOption = new Option(data.name, data.id, true, true);
locationSelect.add(newOption);
// Reset form and close modal
document.getElementById('new-location-name').value = '';
document.getElementById('new-location-description').value = '';
bsModal.hide();
})
.catch(error => {
console.error('Error:', error);
alert('Failed to create location: ' + error.message);
});
});
}
});
</script>
{% endblock %}

View file

@ -35,62 +35,102 @@ def validate_app_data(name, server_id, existing_app_id=None):
return True, None
def is_port_in_use(port_number, protocol, server_id, exclude_app_id=None):
def is_port_in_use(port, server_id, exclude_app_id=None):
"""
Check if a port+protocol combination is already in use by any application on the server
"""
from sqlalchemy import and_
# Join App and Port models to find ports used by apps on this server
query = db.session.query(Port, App).join(App).filter(
Port.port_number == port_number,
Port.protocol == protocol,
App.server_id == server_id
)
# # Exclude the current app if editing
# if exclude_app_id:
# query = query.filter(App.id != exclude_app_id)
result = query.first()
if result:
return True, result.App.name
return False, None
def validate_port_data(port_number, protocol, description, server_id=None, app_id=None):
"""
Validate port data for an application
Check if a port is already in use on a server
Args:
port_number: The port number to validate
protocol: The protocol (TCP/UDP)
description: Port description
server_id: ID of the server
app_id: ID of the application (for excluding the current app when checking conflicts)
port: The port number to check
server_id: The ID of the server
exclude_app_id: Optional app ID to exclude from the check (for editing an app)
Returns:
Tuple of (valid, clean_port, error_message)
bool: True if port is in use, False otherwise
"""
# Check if port number is provided
if not port_number:
return False, None, "Port number is required"
from app.core.models import App, Port
# Get all apps on this server
apps_on_server = App.query.filter_by(server_id=server_id).all()
for app in apps_on_server:
# Skip the app we're editing
if exclude_app_id and app.id == int(exclude_app_id):
continue
# Check if this app uses the port - use port_number rather than number
for app_port in app.ports:
if int(app_port.port_number) == int(port): # Use port_number here
return True
return False
def validate_port_data(ports, descriptions=None, server_id=None, exclude_app_id=None, protocol=None):
"""
Validate port data - works with both the API and form submissions
Args:
ports: List of port numbers or a single port number string
descriptions: List of port descriptions or a single description
server_id: The server ID
exclude_app_id: Optional app ID to exclude from port conflict check
protocol: Optional protocol (for API validation)
Returns:
For form validation: Error message string or None if valid
For API validation: (valid, clean_port, error_message) tuple
"""
# Handle the API call format
if protocol is not None:
# This is the API validation path
try:
port = int(ports)
if port < 1 or port > 65535:
return False, None, f"Port {port} is out of valid range (1-65535)"
# Check if port is already in use
if is_port_in_use(port, server_id, exclude_app_id):
return False, None, f"Port {port} is already in use on this server"
return True, port, None
except ValueError:
return False, None, "Invalid port number"
# Handle the form submission format (list of ports)
seen_ports = set()
# Make sure ports is a list
if not isinstance(ports, list):
ports = [ports]
# Make sure descriptions is a list (or empty list)
if descriptions is None:
descriptions = []
elif not isinstance(descriptions, list):
descriptions = [descriptions]
for i, port_str in enumerate(ports):
if not port_str: # Skip empty port entries
continue
try:
clean_port = int(port_number)
port = int(port_str)
if port < 1 or port > 65535:
return f"Port {port} is out of valid range (1-65535)"
# Check for duplicate ports in the submitted data
if port in seen_ports:
return f"Duplicate port {port} in submission"
seen_ports.add(port)
# Check if port is already in use on this server
if is_port_in_use(port, server_id, exclude_app_id):
return f"Port {port} is already in use on this server"
except ValueError:
return False, None, "Port number must be a valid integer"
return f"Invalid port number: {port_str}"
if clean_port < 1 or clean_port > 65535:
return False, None, "Port number must be between 1 and 65535"
# Always check for port conflicts
in_use, app_name = is_port_in_use(clean_port, protocol, server_id, app_id)
if in_use:
return False, clean_port, f"Port {clean_port}/{protocol} is already in use by application '{app_name}'"
return True, clean_port, None
return None
def process_app_ports(app_id, port_data, server_id=None):
@ -128,7 +168,7 @@ def process_app_ports(app_id, port_data, server_id=None):
# Validate the port data
valid, clean_port, error = validate_port_data(
port_number, protocol, description, server_id, app_id
[port_number], [description], server_id, app_id
)
if not valid:

79
run.py
View file

@ -4,7 +4,7 @@ import importlib.util
from flask import Flask, render_template
from app import create_app
from app.core.extensions import db
from app.core.models import Server, Subnet, App, Port
from app.core.models import Server, Subnet, App, Port, Location
from app.core.auth import User # Import User from auth module
from datetime import datetime
import random
@ -93,6 +93,7 @@ def make_shell_context():
"Subnet": Subnet,
"App": App,
"Port": Port,
"Location": Location,
}
@ -118,63 +119,95 @@ def seed_data():
"""Add some sample data to the database"""
with app.app_context():
# Only seed if the database is empty
if Subnet.query.count() == 0:
if User.query.count() == 0:
# Create a default admin user
admin = User(username="admin", email="admin@example.com", is_admin=True)
admin.set_password("admin")
db.session.add(admin)
db.session.commit()
# Create sample locations
office = Location(
name="Office",
description="Main office network",
user_id=admin.id
)
datacenter = Location(
name="Datacenter",
description="Datacenter network",
user_id=admin.id
)
db.session.add_all([office, datacenter])
db.session.commit()
# Create sample subnets
subnet1 = Subnet(
cidr="192.168.1.0/24", location="Office", active_hosts=json.dumps([])
cidr="192.168.1.0/24",
location_id=office.id,
user_id=admin.id,
active_hosts=json.dumps([])
)
subnet2 = Subnet(
cidr="10.0.0.0/24", location="Datacenter", active_hosts=json.dumps([])
cidr="10.0.0.0/24",
location_id=datacenter.id,
user_id=admin.id,
active_hosts=json.dumps([])
)
db.session.add_all([subnet1, subnet2])
db.session.commit()
# Create sample servers
server1 = Server(
hostname="web-server", ip_address="192.168.1.10", subnet=subnet1
hostname="web-server",
ip_address="192.168.1.10",
subnet_id=subnet1.id,
user_id=admin.id
)
server2 = Server(
hostname="db-server", ip_address="192.168.1.11", subnet=subnet1
hostname="db-server",
ip_address="192.168.1.11",
subnet_id=subnet1.id,
user_id=admin.id
)
server3 = Server(
hostname="app-server", ip_address="10.0.0.5", subnet=subnet2
hostname="app-server",
ip_address="10.0.0.5",
subnet_id=subnet2.id,
user_id=admin.id
)
db.session.add_all([server1, server2, server3])
db.session.commit()
# Create sample apps
app1 = App(
name="Website",
server=server1,
server_id=server1.id,
user_id=admin.id,
documentation="# Company Website\nRunning on Nginx/PHP",
)
app2 = App(
name="PostgreSQL",
server=server2,
server_id=server2.id,
user_id=admin.id,
documentation="# Database Server\nPostgreSQL 15",
)
app3 = App(
name="API Service",
server=server3,
server_id=server3.id,
user_id=admin.id,
documentation="# REST API\nNode.js service",
)
db.session.add_all([app1, app2, app3])
db.session.commit()
# Create sample ports
port1 = Port(app=app1, port_number=80, protocol="TCP", description="HTTP")
port2 = Port(app=app1, port_number=443, protocol="TCP", description="HTTPS")
port3 = Port(
app=app2, port_number=5432, protocol="TCP", description="PostgreSQL"
)
port4 = Port(
app=app3, port_number=3000, protocol="TCP", description="Node.js API"
)
db.session.add_all([port1, port2, port3, port4])
ports = [
Port(app_id=app1.id, port_number=80, protocol="TCP", description="HTTP"),
Port(app_id=app1.id, port_number=443, protocol="TCP", description="HTTPS"),
Port(app_id=app2.id, port_number=5432, protocol="TCP", description="PostgreSQL"),
Port(app_id=app3.id, port_number=3000, protocol="TCP", description="Node.js API")
]
db.session.add_all(ports)
db.session.commit()
print("Sample data has been added to the database")

View file

@ -19,4 +19,4 @@ application = app
if __name__ == "__main__":
# Only for development
debug = flask_env != "production"
app.run(host="0.0.0.0", port=8000, debug=debug)
app.run(host="0.0.0.0", port=5001, debug=debug)