Files
3cloud-backend/app/models/network.py
T
JamesBhattarai 7ec2e4d47b Feat: Implement NAT for Private network
Added enable_nat bool for networks to allow nats
Implemented docker network bridge, to allow nat with tenancy seperated as `enable_icc:false`
2026-08-08 17:41:15 +02:00

204 lines
8.2 KiB
Python

import uuid
import ipaddress
from sqlalchemy.orm import Session, relationship
from datetime import datetime
from sqlalchemy import (
Column, String, Boolean, DateTime, ForeignKey, Integer, Float, Text
)
from .models import BaseModel
import re
import uuid
from sqlalchemy.orm import validates
from sqlalchemy.exc import IntegrityError
from sqlalchemy import and_
OVS_PORT_NAME_REGEX = r"^[a-zA-Z0-9._\-]{1,15}$"
def generate_unique_port_name(session, base_name="port"):
"""Generates a unique OVS-compliant port name."""
for _ in range(10): # Try up to 10 times
candidate = f"{base_name[:8]}-{uuid.uuid4().hex[:6]}"
if not session.query(NetworkPort).filter_by(name=candidate).first():
return candidate
raise RuntimeError("Failed to generate a unique port name")
class NetworkPort(BaseModel):
__tablename__ = "network_ports"
network_id = Column(String(36), ForeignKey("networks.id"), nullable=False)
workload_id = Column(String(36), ForeignKey("workloads.id"), nullable=True)
name = Column(String(255), unique=True, nullable=True)
ip_address = Column(String(50), nullable=True)
mac_address = Column(String(20), nullable=True)
port_type = Column(String(50), nullable=True)
dns_servers = Column(String(255), nullable=True)
subnet_mask = Column(String(50), nullable=True)
network = relationship("Network")
workload = relationship("Workload", back_populates="network_ports")
def __init__(self, *args, session=None, **kwargs):
supplied_name = kwargs.get("name")
session = session or kwargs.pop("_session", None)
# Convert UUID strings to UUID objects for any UUID fields
for key, value in kwargs.items():
if key.endswith('_id') and isinstance(value, str):
try:
kwargs[key] = str(value) if value else None
except (ValueError, TypeError):
pass
if not supplied_name or not re.match(OVS_PORT_NAME_REGEX, supplied_name) or (
session and session.query(NetworkPort).filter_by(name=supplied_name).first()
):
if not session:
raise ValueError("Session is required to generate a unique port name")
kwargs["name"] = generate_unique_port_name(session)
else:
kwargs["name"] = supplied_name
super().__init__(*args, **kwargs)
class Network(BaseModel):
__tablename__ = "networks"
vdc_id = Column(String(36), ForeignKey("vdcs.id"), nullable=False)
ipv4_cidr = Column(String(50), nullable=True)
ipv4_gateway = Column(String(50), nullable=True)
ipv4_dns_servers = Column(String(255), nullable=True)
dhcp_range_start = Column(String(50), nullable=True)
dhcp_range_end = Column(String(50), nullable=True)
nscontroller_ip = Column(String(50), nullable=True)
ipv6_cidr = Column(String(50), nullable=True)
ipv6_gateway = Column(String(50), nullable=True)
vni = Column(Integer, nullable=False)
encapsulation = Column(String(50), default="VXLAN", nullable=False)
ovs_bridge = Column(String(255), nullable=True)
network_type = Column(String(255), nullable=True)
enable_host_nat = Column(Boolean, default=False, nullable=False, server_default="0")
def __init__(self, *args, **kwargs):
# Convert UUID strings to UUID objects for any UUID fields
for key, value in kwargs.items():
if key.endswith('_id') and isinstance(value, str):
try:
kwargs[key] = str((value)) if value else None
except (ValueError, TypeError):
pass
super().__init__(*args, **kwargs)
def create_port(self, db: Session, workload_id: str = None, name: str = None, port_type: str = None, ip_address: str = None, use_dhcp_range: bool = False):
# Convert workload_id to string if it's a UUID object
if workload_id and hasattr(workload_id, 'hex'):
workload_id = str(workload_id)
# Define the prefix
prefix = "fa:16:4a"
# Generate the MAC address with the specified prefix
mac_address = prefix + ':' + ':'.join(f'{byte:02x}' for byte in uuid.uuid4().bytes[-3:])
# Generate a unique IP address within the network's CIDR range (or use provided one)
ip_network = ipaddress.ip_network(self.ipv4_cidr)
used_ips = {port.ip_address for port in db.query(NetworkPort).filter(
NetworkPort.network_id == self.id,
NetworkPort.deleted == False
).all()}
if not ip_address:
reserved_ips = set()
if self.ipv4_gateway:
reserved_ips.add(self.ipv4_gateway)
if self.nscontroller_ip:
reserved_ips.add(self.nscontroller_ip)
dhcp_start = None
dhcp_end = None
if self.dhcp_range_start and self.dhcp_range_end:
try:
dhcp_start = ipaddress.ip_address(self.dhcp_range_start)
dhcp_end = ipaddress.ip_address(self.dhcp_range_end)
except ValueError:
dhcp_start = dhcp_end = None
for ip in ip_network.hosts():
candidate = str(ip)
if candidate in used_ips or candidate in reserved_ips:
continue
in_dhcp_range = bool(dhcp_start and dhcp_end and dhcp_start <= ip <= dhcp_end)
if in_dhcp_range != use_dhcp_range:
continue
ip_address = candidate
break
else:
raise ValueError("No available IP addresses in the network")
else:
# Validate provided IP is in network and not already used
if port_type != "NSController" and ip_address in used_ips:
raise ValueError(f"IP address {ip_address} is already in use")
if not ipaddress.ip_address(ip_address) in ip_network:
raise ValueError(f"IP address {ip_address} is not in network {self.ipv4_cidr}")
# Check for MAC address conflicts
if db.query(NetworkPort).filter(NetworkPort.mac_address == mac_address, NetworkPort.deleted == False).first():
raise ValueError("MAC address conflict detected")
# Create the new port
new_port = NetworkPort(
network_id=self.id,
workload_id=workload_id,
ip_address=ip_address,
mac_address=mac_address,
port_type=port_type,
dns_servers=self.ipv4_dns_servers,
subnet_mask=str(ip_network.netmask),
status='allocated',
session=db
)
db.add(new_port)
db.commit()
db.refresh(new_port)
return new_port
class PodDnsRecord(BaseModel):
"""
Custom DNS records for pods (aliases only).
Automatic A and SRV records are generated from pod metadata,
this table only stores user-defined custom aliases.
"""
__tablename__ = 'pod_dns_records'
# Foreign keys
vdc_id = Column(String(36), ForeignKey('vdcs.id'), nullable=False, index=True)
target_pod_id = Column(String(36), ForeignKey('container_pods.id'), nullable=True, index=True)
# DNS record fields
custom_fqdn = Column(String(255), nullable=False, index=True) # e.g., "api.myapp.mycloud.local"
record_type = Column(String(10), nullable=False, default='A') # A, CNAME, etc.
target_value = Column(String(255), nullable=False) # IP address or target FQDN
ttl = Column(Integer, nullable=False, default=300) # DNS TTL in seconds
# Relationships
vdc = relationship('VirtualDataCenter', backref='dns_records')
target_pod = relationship('ContainerPod', backref='dns_aliases')
def to_dict(self):
"""Convert to dictionary for API responses"""
return {
'id': self.id,
'vdc_id': self.vdc_id,
'target_pod_id': self.target_pod_id,
'custom_fqdn': self.custom_fqdn,
'record_type': self.record_type,
'target_value': self.target_value,
'ttl': self.ttl,
'description': self.description,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}