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