Added enable_nat bool for networks to allow nats Implemented docker network bridge, to allow nat with tenancy seperated as `enable_icc:false`
204 lines
8.2 KiB
Python
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
|
|
} |