Files
3cloud-backend/app/controller/api/network_routes.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

478 lines
16 KiB
Python

from flask import request
from app import app, db, logger
from app.models.network import Network, NetworkPort
from app.models.models import VirtualDataCenter, AuditEntry, Workload
from app.utils.standard_responses import api_response
from datetime import datetime
from app.controller import api_bp
import uuid
from uuid import UUID
from app.utils.auth_utils import get_request_user_id
from app.utils.dns_helpers import send_dns_updates_for_vdc
from app.utils.sdn_helpers import preview_sdn_updates_for_networks
import random
def validate_network_data(data, is_update=False):
"""
Validates the incoming network data.
Args:
data (dict): The incoming JSON payload.
is_update (bool): Whether this is an update operation (some fields may be optional).
Returns:
dict: The validated data.
Raises:
HTTPException: If validation fails, aborts with a 400 status code and error message.
"""
errors = []
# Required fields for creation
if not is_update:
if 'name' not in data:
errors.append("Missing required field: name")
if 'vdc_id' not in data:
errors.append("Missing required field: vdc_id")
# Validate name (if present)
if 'name' in data and not isinstance(data['name'], str):
errors.append("Field 'name' must be a string")
# Validate vdc_id (if present)
if 'vdc_id' in data:
try:
UUID(data['vdc_id']) # Ensure it's a valid UUID
except ValueError:
errors.append("Field 'vdc_id' must be a valid UUID")
# Validate vni (if present)
if 'vni' in data and not isinstance(data['vni'], int):
errors.append("Field 'vni' must be an integer")
# Validate optional fields
optional_fields = {
'ipv4_cidr': str,
'ipv4_gateway': str,
'ipv4_dns_servers': str,
'dhcp_range_start': str,
'dhcp_range_end': str,
'nscontroller_ip': str,
'ipv6_cidr': str,
'ipv6_gateway': str,
'ovs_bridge': str,
'enable_host_nat': bool,
'encapsulation': str,
}
for field, field_type in optional_fields.items():
if field in data and not isinstance(data[field], field_type):
errors.append(f"Field '{field}' must be of type {field_type.__name__}")
if errors:
raise ValueError(", ".join(errors))
return data
@api_bp.route('/networks', methods=['POST'])
def add_network():
data = request.json
# Validate incoming data
try:
validated_data = validate_network_data(data)
except ValueError as ve:
return api_response(
success=False,
message="Validation failed",
status=400,
error_type="VALIDATION_ERROR",
error_details={"errors": str(ve)}
)
# Handle VNI: either validate provided one or generate a new one
vdc_id = validated_data['vdc_id']
# Get the region of the VDC
vdc = VirtualDataCenter.query.get(vdc_id)
if not vdc:
return api_response(
success=False,
message="Virtual Data Center not found",
status=404,
error_type="NOT_FOUND"
)
region_id = vdc.region_id
# Get all networks in the same region
networks = Network.query.join(VirtualDataCenter, Network.vdc_id == VirtualDataCenter.id).filter(
VirtualDataCenter.region_id == region_id,
Network.deleted == False
).all()
existing_vnis = {net.vni for net in networks}
if 'vni' in validated_data:
# Validate that the provided VNI is unique in the region
if validated_data['vni'] in existing_vnis:
return api_response(
success=False,
message="VNI already in use in this region",
status=409,
error_type="VNI_CONFLICT",
error_details={"vni": validated_data['vni']}
)
else:
# Generate an available VNI
while True:
vni = random.randint(100, 4000)
if vni not in existing_vnis:
break
validated_data['vni'] = vni
# Create the Network instance
instance = Network(
name=validated_data['name'],
vdc_id=UUID(validated_data['vdc_id']),
vni=validated_data['vni'],
ipv4_cidr=validated_data.get('ipv4_cidr'),
ipv4_gateway=validated_data.get('ipv4_gateway'),
ipv4_dns_servers=validated_data.get('ipv4_dns_servers'),
dhcp_range_start=validated_data.get('dhcp_range_start'),
dhcp_range_end=validated_data.get('dhcp_range_end'),
nscontroller_ip=validated_data.get('nscontroller_ip'),
ipv6_cidr=validated_data.get('ipv6_cidr'),
ipv6_gateway=validated_data.get('ipv6_gateway'),
ovs_bridge=validated_data.get('ovs_bridge'),
enable_host_nat=validated_data.get('enable_host_nat', False),
**({'encapsulation': validated_data['encapsulation']} if 'encapsulation' in validated_data else {}),
)
db.session.add(instance)
db.session.commit()
logger.debug("Network added to DB")
# Audit
try:
user_id = get_request_user_id(request)
AuditEntry.log_event(
object=instance,
action="network_created",
description=f"name={instance.name} vdc_id={instance.vdc_id} vni={instance.vni}",
user_id=user_id
)
except Exception as _e:
logger.error(f"Audit logging failed for network create: {_e}")
return api_response(data=instance.to_json(), status=201, message="Network created successfully")
@api_bp.route('/networks/<network_id>', methods=['PUT'])
def edit_network(network_id):
network = Network.query.get_or_404(network_id)
data = request.json
# Validate incoming data
try:
validated_data = validate_network_data(data, is_update=True)
except ValueError as ve:
return api_response(
success=False,
message="Validation failed",
status=400,
error_type="VALIDATION_ERROR",
error_details={"errors": str(ve)}
)
# Update the fields
for key, value in validated_data.items():
setattr(network, key, value)
db.session.commit()
# Audit
try:
user_id = get_request_user_id(request)
fields = ", ".join(list(validated_data.keys()))
AuditEntry.log_event(
object=network,
action="network_updated",
description=f"Updated fields: {fields}" if fields else "Updated",
user_id=user_id
)
except Exception as _e:
logger.error(f"Audit logging failed for network update {network_id}: {_e}")
return api_response(message="Network updated successfully")
@api_bp.route('/networks/<network_id>', methods=['GET'])
def get_network(network_id):
network = Network.query.get_or_404((network_id))
return api_response(data=network.to_json())
@api_bp.route('/networks/<network_id>', methods=['DELETE'])
def delete_network(network_id):
network = Network.query.get_or_404(network_id)
network.soft_delete()
db.session.commit()
# Audit
try:
user_id = get_request_user_id(request)
AuditEntry.log_event(
object=network,
action="network_deleted",
description=f"name={network.name}",
user_id=user_id
)
except Exception as _e:
logger.error(f"Audit logging failed for network delete {network_id}: {_e}")
return api_response(message="Network deleted successfully")
@api_bp.route('/networks', methods=['GET'])
def get_networks():
# Get the virtual_datacenter_id query parameter if provided
virtual_datacenter_id = request.args.get('virtual_datacenter_id')
# Validate and check VDC if provided
if virtual_datacenter_id:
logger.debug("Got vdc")
# Validate UUID format
try:
UUID(virtual_datacenter_id)
except ValueError:
return api_response(
success=False,
message="virtual_datacenter_id must be a valid UUID",
status=400,
error_type="INVALID_ID_FORMAT"
)
# Check if VDC exists and is not deleted
vdc = VirtualDataCenter.query.filter_by(id=virtual_datacenter_id, deleted=False).first()
if not vdc:
return api_response(
success=False,
message="Virtual Data Center not found or deleted",
status=404,
error_type="NOT_FOUND"
)
logger.debug("VDC looks good")
# Filter networks by VDC ID
networks = Network.query.filter_by(vdc_id=virtual_datacenter_id).order_by(Network.name.asc()).all()
logger.debug(networks)
else:
logger.debug("No VDC, checkign all")
# Get all networks if no VDC filter is provided
networks = Network.query.filter_by(deleted=False).order_by(Network.name.asc()).all()
return api_response(data=[network.to_json() for network in networks])
def validate_network_port_data(data, is_update=False):
"""
Validates the incoming network port data.
Args:
data (dict): The incoming JSON payload.
is_update (bool): Whether this is an update operation (some fields may be optional).
Returns:
dict: The validated data.
Raises:
HTTPException: If validation fails, aborts with a 400 status code and error message.
"""
errors = []
# Required fields for creation
if not is_update:
if 'network_id' not in data:
errors.append("Missing required field: network_id")
if 'workload_id' not in data:
errors.append("Missing required field: workload_id")
# Validate network_id and workload_id (if present)
for field in ['network_id', 'workload_id']:
if field in data:
try:
UUID(data[field]) # Ensure it's a valid UUID
except ValueError:
errors.append(f"Field '{field}' must be a valid UUID")
# Validate optional fields
optional_fields = {
'ip_address': str,
'mac_address': str,
'port_type': str,
'gateway': str,
'dns_servers': str,
'subnet_mask': str,
'state': str,
}
for field, field_type in optional_fields.items():
if field in data and not isinstance(data[field], field_type):
errors.append(f"Field '{field}' must be of type {field_type.__name__}")
if errors:
raise ValueError(", ".join(errors))
return data
@api_bp.route('/network_ports', methods=['POST'])
def add_network_port():
data = request.json
# Validate incoming data
try:
validated_data = validate_network_port_data(data)
except ValueError as ve:
return api_response(
success=False,
message="Validation failed",
status=400,
error_type="VALIDATION_ERROR",
error_details={"errors": str(ve)}
)
# Create the NetworkPort instance
instance = NetworkPort(
network_id=UUID(validated_data['network_id']),
workload_id=UUID(validated_data['workload_id']),
ip_address=validated_data.get('ip_address'),
mac_address=validated_data.get('mac_address'),
port_type=validated_data.get('port_type'),
gateway=validated_data.get('gateway'),
dns_servers=validated_data.get('dns_servers'),
subnet_mask=validated_data.get('subnet_mask'),
state=validated_data.get('state')
)
db.session.add(instance)
db.session.commit()
logger.debug("NetworkPort added to DB")
# Audit
try:
user_id = get_request_user_id(request)
AuditEntry.log_event(
object=instance,
action="network_port_created",
description=f"network_id={instance.network_id} workload_id={instance.workload_id} ip={instance.ip_address}",
user_id=user_id
)
except Exception as _e:
logger.error(f"Audit logging failed for network_port create: {_e}")
return api_response(data=instance.to_json(), status=201, message="NetworkPort created successfully")
@api_bp.route('/network_ports/<network_port_id>', methods=['PUT'])
def edit_network_port(network_port_id):
network_port = NetworkPort.query.get_or_404(network_port_id)
data = request.json
# Validate incoming data
try:
validated_data = validate_network_port_data(data, is_update=True)
except ValueError as ve:
return api_response(
success=False,
message="Validation failed",
status=400,
error_type="VALIDATION_ERROR",
error_details={"errors": str(ve)}
)
# Track if IP address changed for DNS update
ip_changed = 'ip_address' in validated_data and validated_data['ip_address'] != network_port.ip_address
# Update the fields
for key, value in validated_data.items():
setattr(network_port, key, value)
db.session.commit()
# Trigger DNS update if IP address changed on an NSController workload
if ip_changed:
try:
# Get the workload associated with this network port
workload = Workload.query.get(network_port.workload_id)
if workload and workload.workload_type == "NSController" and workload.pod_id:
# Get the pod to find the VDC
from app.models.models import ContainerPod
pod = ContainerPod.query.get(workload.pod_id)
if pod:
send_dns_updates_for_vdc(pod.vdc_id, logger)
logger.info(f"DNS updates sent for VDC {pod.vdc_id} after NetworkPort IP change")
except Exception as e:
logger.error(f"Failed to send DNS updates after NetworkPort update: {e}")
# Don't fail the network port update, just log the error
# Audit
try:
user_id = get_request_user_id(request)
fields = ", ".join(list(validated_data.keys()))
AuditEntry.log_event(
object=network_port,
action="network_port_updated",
description=f"Updated fields: {fields}" if fields else "Updated",
user_id=user_id
)
except Exception as _e:
logger.error(f"Audit logging failed for network_port update {network_port_id}: {_e}")
return api_response(message="NetworkPort updated successfully")
@api_bp.route('/network_ports/by_network/<network_id>', methods=['GET'])
def get_network_ports_by_network(network_id):
if not network_id:
return api_response(
success=False,
message="Network ID is required",
status=404,
error_type="NOT_FOUND"
)
network_ports = NetworkPort.query.filter_by(network_id=(network_id)).all()
return api_response(data=[network_port.to_json() for network_port in network_ports])
@api_bp.route('/network_ports', methods=['GET'])
def get_network_ports():
network_ports = NetworkPort.query.all()
return api_response(data=[network_port.to_json() for network_port in network_ports])
@api_bp.route('/network_ports/<network_port_id>', methods=['GET'])
def get_network_port(network_port_id):
network_port = NetworkPort.query.get_or_404(network_port_id)
return api_response(data=network_port.to_json())
@api_bp.route('/network_ports/<network_port_id>', methods=['DELETE'])
def delete_network_port(network_port_id):
network_port = NetworkPort.query.get_or_404(network_port_id)
network_port.soft_delete()
db.session.commit()
# Audit
try:
user_id = get_request_user_id(request)
AuditEntry.log_event(
object=network_port,
action="network_port_deleted",
description=f"network_id={network_port.network_id} workload_id={network_port.workload_id} ip={network_port.ip_address}",
user_id=user_id
)
except Exception as _e:
logger.error(f"Audit logging failed for network_port delete {network_port_id}: {_e}")
return api_response(message="NetworkPort deleted successfully")
@api_bp.route('/networks/<network_id>/debug/sdn-payload', methods=['GET'])
def debug_sdn_payload(network_id):
"""
Debug endpoint to preview SDN payload that would be sent to workers.
Returns the exact payload structure that would be sent via send_sdn_updates_for_networks.
"""
if not network_id:
return api_response(
success=False,
message="network_id required",
status=400,
error_type="VALIDATION_ERROR"
)
payloads = preview_sdn_updates_for_networks([network_id])
return api_response(data={"payloads": payloads})