import { isIP } from "node:net"; import { IpAllocationStrategy, IpAddressStatus, IpAssignmentType, IpScope, IpVersion, Prisma, PrivateNetworkAttachmentStatus, PrivateNetworkType, VmType } from "@prisma/client"; import { prisma } from "../lib/prisma"; import { HttpError } from "../lib/http-error"; import { toPrismaJsonValue } from "../lib/prisma-json"; import { reconfigureVmNetwork, updateVmConfiguration } from "./proxmox.service"; type ListIpAddressInput = { status?: IpAddressStatus; version?: IpVersion; scope?: IpScope; nodeHostname?: string; bridge?: string; vlanTag?: number; assignedVmId?: string; limit?: number; offset?: number; }; type ImportIpAddressesInput = { addresses?: string[]; cidr_blocks?: string[]; scope?: IpScope; server?: string; node_id?: string; node_hostname?: string; bridge?: string; vlan_tag?: number; sdn_zone?: string; gateway?: string; subnet?: string; tags?: string[]; metadata?: Record; imported_by?: string; }; type AssignIpInput = { vm_id: string; ip_address_id?: string; address?: string; scope?: IpScope; version?: IpVersion; assignment_type?: IpAssignmentType; interface_name?: string; notes?: string; actor_email?: string; metadata?: Record; }; type ReturnIpInput = { assignment_id?: string; ip_address_id?: string; }; type CreatePrivateNetworkInput = { name: string; slug?: string; network_type?: PrivateNetworkType; cidr: string; gateway?: string; bridge?: string; vlan_tag?: number; sdn_zone?: string; server?: string; node_hostname?: string; metadata?: Record; created_by?: string; }; type AttachPrivateNetworkInput = { network_id: string; vm_id: string; interface_name?: string; requested_ip?: string; actor_email?: string; metadata?: Record; }; type DetachPrivateNetworkInput = { attachment_id: string; actor_email?: string; }; type UpsertTenantQuotaInput = { tenant_id: string; ipv4_limit?: number | null; ipv6_limit?: number | null; reserved_ipv4?: number; reserved_ipv6?: number; burst_allowed?: boolean; burst_ipv4_limit?: number | null; burst_ipv6_limit?: number | null; is_active?: boolean; metadata?: Record; created_by?: string; }; type CreateReservedRangeInput = { name: string; cidr: string; scope?: IpScope; tenant_id?: string; reason?: string; node_hostname?: string; bridge?: string; vlan_tag?: number; sdn_zone?: string; is_active?: boolean; metadata?: Record; created_by?: string; }; type UpsertIpPoolPolicyInput = { policy_id?: string; name: string; tenant_id?: string; scope?: IpScope; version?: IpVersion; node_hostname?: string; bridge?: string; vlan_tag?: number; sdn_zone?: string; allocation_strategy?: IpAllocationStrategy; enforce_quota?: boolean; disallow_reserved_use?: boolean; is_active?: boolean; priority?: number; metadata?: Record; created_by?: string; }; function normalizeSlug(value: string) { return value .toLowerCase() .trim() .replace(/[^a-z0-9\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-"); } function parseIpVersion(address: string): IpVersion { const version = isIP(address); if (version === 4) return IpVersion.IPV4; if (version === 6) return IpVersion.IPV6; throw new HttpError(400, `Invalid IP address: ${address}`, "INVALID_IP_ADDRESS"); } function parseCidr(cidr: string): { address: string; prefix: number; version: IpVersion } { const trimmed = cidr.trim(); const [address, rawPrefix] = trimmed.split("/"); if (!address || !rawPrefix) { throw new HttpError(400, `Invalid CIDR block: ${cidr}`, "INVALID_CIDR"); } const version = parseIpVersion(address); const prefix = Number(rawPrefix); const maxPrefix = version === IpVersion.IPV4 ? 32 : 128; if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) { throw new HttpError(400, `CIDR prefix out of range: ${cidr}`, "INVALID_CIDR"); } return { address, prefix, version }; } function ipv4ToInt(address: string) { const parts = address.split(".").map((value) => Number(value)); if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) { throw new HttpError(400, `Invalid IPv4 address: ${address}`, "INVALID_IPV4"); } return (((parts[0] << 24) >>> 0) + ((parts[1] << 16) >>> 0) + ((parts[2] << 8) >>> 0) + (parts[3] >>> 0)) >>> 0; } function intToIpv4(value: number) { return `${(value >>> 24) & 255}.${(value >>> 16) & 255}.${(value >>> 8) & 255}.${value & 255}`; } function expandIpv4Cidr(address: string, prefix: number): string[] { const hostBits = 32 - prefix; if (hostBits > 12) { throw new HttpError(400, `CIDR block too large for bulk import (${address}/${prefix})`, "CIDR_TOO_LARGE"); } const size = 2 ** hostBits; const mask = prefix === 0 ? 0 : (0xffffffff << hostBits) >>> 0; const base = ipv4ToInt(address) & mask; const output: string[] = []; for (let index = 0; index < size; index += 1) { output.push(intToIpv4((base + index) >>> 0)); } return output; } function normalizeIpv6(address: string) { if (!address.includes(":")) { throw new HttpError(400, `Invalid IPv6 address: ${address}`, "INVALID_IPV6"); } const [leftPart, rightPart] = address.split("::"); if (address.split("::").length > 2) { throw new HttpError(400, `Invalid IPv6 address: ${address}`, "INVALID_IPV6"); } const left = leftPart ? leftPart.split(":").filter((item) => item.length > 0) : []; const right = rightPart ? rightPart.split(":").filter((item) => item.length > 0) : []; const missing = 8 - (left.length + right.length); if (missing < 0) { throw new HttpError(400, `Invalid IPv6 address: ${address}`, "INVALID_IPV6"); } const groups = [...left, ...Array.from({ length: missing }, () => "0"), ...right]; if (groups.length !== 8) { throw new HttpError(400, `Invalid IPv6 address: ${address}`, "INVALID_IPV6"); } return groups.map((group) => { const parsed = Number.parseInt(group, 16); if (!Number.isInteger(parsed) || parsed < 0 || parsed > 0xffff) { throw new HttpError(400, `Invalid IPv6 address: ${address}`, "INVALID_IPV6"); } return parsed; }); } function ipv6ToBigInt(address: string) { const groups = normalizeIpv6(address); let output = 0n; for (const group of groups) { output = (output << 16n) + BigInt(group); } return output; } function ipToBigInt(address: string, version: IpVersion) { if (version === IpVersion.IPV4) { return BigInt(ipv4ToInt(address)); } return ipv6ToBigInt(address); } function cidrBounds(cidr: string) { const parsed = parseCidr(cidr); const bits = parsed.version === IpVersion.IPV4 ? 32 : 128; const base = ipToBigInt(parsed.address, parsed.version); const hostBits = BigInt(bits - parsed.prefix); const size = 1n << hostBits; const mask = ((1n << BigInt(bits)) - 1n) ^ (size - 1n); const network = base & mask; const broadcast = network + size - 1n; return { ...parsed, network, broadcast }; } function addressInCidr(address: string, cidr: string) { const bounds = cidrBounds(cidr); const addressVersion = parseIpVersion(address); if (addressVersion !== bounds.version) { return false; } const numeric = ipToBigInt(address, addressVersion); return numeric >= bounds.network && numeric <= bounds.broadcast; } function samePlacementContext( candidate: { node_hostname: string | null; bridge: string | null; vlan_tag: number | null; sdn_zone: string | null }, selector: { node_hostname?: string | null; bridge?: string | null; vlan_tag?: number | null; sdn_zone?: string | null } ) { if (selector.node_hostname && candidate.node_hostname && selector.node_hostname !== candidate.node_hostname) return false; if (selector.bridge && candidate.bridge && selector.bridge !== candidate.bridge) return false; if (typeof selector.vlan_tag === "number" && typeof candidate.vlan_tag === "number" && selector.vlan_tag !== candidate.vlan_tag) return false; if (selector.sdn_zone && candidate.sdn_zone && selector.sdn_zone !== candidate.sdn_zone) return false; return true; } async function fetchVmForNetwork(vmId: string) { const vm = await prisma.virtualMachine.findUnique({ where: { id: vmId }, select: { id: true, vmid: true, name: true, node: true, type: true, tenant_id: true, ip_address: true } }); if (!vm) { throw new HttpError(404, "VM not found", "VM_NOT_FOUND"); } return vm; } function vmRuntime(vmType: VmType): "qemu" | "lxc" { return vmType === VmType.LXC ? "lxc" : "qemu"; } async function resolveAllocationPolicy(input: { tenant_id: string; scope?: IpScope; version?: IpVersion; node_hostname?: string | null; bridge?: string | null; vlan_tag?: number | null; sdn_zone?: string | null; }) { const policies = await prisma.ipPoolPolicy.findMany({ where: { is_active: true, OR: [{ tenant_id: input.tenant_id }, { tenant_id: null }] }, orderBy: [{ priority: "asc" }, { created_at: "asc" }] }); const matches = policies.filter((policy) => { if (policy.tenant_id && policy.tenant_id !== input.tenant_id) return false; if (policy.scope && input.scope && policy.scope !== input.scope) return false; if (policy.version && input.version && policy.version !== input.version) return false; if (policy.node_hostname && input.node_hostname && policy.node_hostname !== input.node_hostname) return false; if (policy.bridge && input.bridge && policy.bridge !== input.bridge) return false; if (typeof policy.vlan_tag === "number" && typeof input.vlan_tag === "number" && policy.vlan_tag !== input.vlan_tag) return false; if (policy.sdn_zone && input.sdn_zone && policy.sdn_zone !== input.sdn_zone) return false; return true; }); const scored = matches .map((policy) => { let score = 0; if (policy.tenant_id) score += 100; if (policy.scope) score += 10; if (policy.version) score += 10; if (policy.node_hostname) score += 4; if (policy.bridge) score += 3; if (typeof policy.vlan_tag === "number") score += 2; if (policy.sdn_zone) score += 1; score -= policy.priority / 1000; return { policy, score }; }) .sort((a, b) => b.score - a.score); return scored[0]?.policy ?? null; } async function enforceTenantQuota(input: { tenant_id: string; version: IpVersion; policyEnforced: boolean }) { if (!input.policyEnforced) { return; } const quota = await prisma.tenantIpQuota.findUnique({ where: { tenant_id: input.tenant_id } }); if (!quota || !quota.is_active) { return; } const [assignedV4, assignedV6] = await Promise.all([ prisma.ipAssignment.count({ where: { tenant_id: input.tenant_id, is_active: true, ip_address: { version: IpVersion.IPV4 } } }), prisma.ipAssignment.count({ where: { tenant_id: input.tenant_id, is_active: true, ip_address: { version: IpVersion.IPV6 } } }) ]); const v4Limit = quota.ipv4_limit ?? null; const v6Limit = quota.ipv6_limit ?? null; const v4Effective = quota.burst_allowed ? quota.burst_ipv4_limit ?? v4Limit : v4Limit; const v6Effective = quota.burst_allowed ? quota.burst_ipv6_limit ?? v6Limit : v6Limit; if (input.version === IpVersion.IPV4 && typeof v4Effective === "number" && assignedV4 >= v4Effective) { throw new HttpError(409, "Tenant IPv4 quota exhausted", "TENANT_IPV4_QUOTA_EXCEEDED"); } if (input.version === IpVersion.IPV6 && typeof v6Effective === "number" && assignedV6 >= v6Effective) { throw new HttpError(409, "Tenant IPv6 quota exhausted", "TENANT_IPV6_QUOTA_EXCEEDED"); } } function isCandidateReservedForOtherTenant( candidate: { address: string; scope: IpScope; version: IpVersion; node_hostname: string | null; bridge: string | null; vlan_tag: number | null; sdn_zone: string | null; }, ranges: Array<{ tenant_id: string | null; cidr: string; scope: IpScope; version: IpVersion; node_hostname: string | null; bridge: string | null; vlan_tag: number | null; sdn_zone: string | null; }>, tenantId: string ) { const matches = ranges.filter((range) => { if (range.scope !== candidate.scope) return false; if (range.version !== candidate.version) return false; if (!samePlacementContext(candidate, range)) return false; return addressInCidr(candidate.address, range.cidr); }); if (matches.length === 0) return { blocked: false, tenantReserved: false }; if (matches.some((range) => range.tenant_id === tenantId)) return { blocked: false, tenantReserved: true }; return { blocked: true, tenantReserved: false }; } function pickBestFitCandidate( candidates: Array<{ id: string; address: string; cidr: number; version: IpVersion; subnet: string | null; imported_at: Date; tenantReserved: boolean; }>, strategy: IpAllocationStrategy ) { if (candidates.length === 0) return null; if (strategy === IpAllocationStrategy.FIRST_AVAILABLE) { return [...candidates].sort((a, b) => a.imported_at.getTime() - b.imported_at.getTime() || a.address.localeCompare(b.address))[0]; } const groupAvailability = candidates.reduce>((acc, item) => { const key = item.subnet ?? `${item.address}/${item.cidr}`; acc[key] = (acc[key] ?? 0) + 1; return acc; }, {}); return [...candidates].sort((a, b) => { if (a.tenantReserved !== b.tenantReserved) return a.tenantReserved ? -1 : 1; const aGroup = groupAvailability[a.subnet ?? `${a.address}/${a.cidr}`] ?? 0; const bGroup = groupAvailability[b.subnet ?? `${b.address}/${b.cidr}`] ?? 0; if (aGroup !== bGroup) return aGroup - bGroup; if (a.cidr !== b.cidr) return b.cidr - a.cidr; return a.imported_at.getTime() - b.imported_at.getTime() || a.address.localeCompare(b.address); })[0]; } export async function listIpAddresses(input: ListIpAddressInput) { const where: Prisma.IpAddressPoolWhereInput = {}; if (input.status) where.status = input.status; if (input.version) where.version = input.version; if (input.scope) where.scope = input.scope; if (input.nodeHostname) where.node_hostname = input.nodeHostname; if (input.bridge) where.bridge = input.bridge; if (typeof input.vlanTag === "number") where.vlan_tag = input.vlanTag; if (input.assignedVmId) where.assigned_vm_id = input.assignedVmId; const limit = Math.min(Math.max(input.limit ?? 100, 1), 500); const offset = Math.max(input.offset ?? 0, 0); const [data, total] = await Promise.all([ prisma.ipAddressPool.findMany({ where, include: { assigned_vm: { select: { id: true, name: true, tenant_id: true } } }, orderBy: [{ status: "asc" }, { address: "asc" }], take: limit, skip: offset }), prisma.ipAddressPool.count({ where }) ]); return { data, meta: { total, limit, offset } }; } export async function importIpAddresses(input: ImportIpAddressesInput) { const baseTags = Array.isArray(input.tags) ? input.tags.filter((item) => item.trim().length > 0) : []; const metadata = input.metadata ? toPrismaJsonValue(input.metadata) : {}; const directAddresses = Array.isArray(input.addresses) ? input.addresses : []; const cidrBlocks = Array.isArray(input.cidr_blocks) ? input.cidr_blocks : []; const prepared: Array<{ address: string; cidr: number; version: IpVersion; status: IpAddressStatus; subnet: string | null; }> = []; for (const raw of directAddresses) { const address = raw.trim(); const version = parseIpVersion(address); prepared.push({ address, cidr: version === IpVersion.IPV4 ? 32 : 128, version, status: IpAddressStatus.AVAILABLE, subnet: input.subnet ?? null }); } for (const block of cidrBlocks) { const parsed = parseCidr(block); if (parsed.version === IpVersion.IPV4) { const expanded = expandIpv4Cidr(parsed.address, parsed.prefix); for (const address of expanded) { prepared.push({ address, cidr: parsed.prefix, version: parsed.version, status: IpAddressStatus.AVAILABLE, subnet: block }); } continue; } if (parsed.prefix === 128) { prepared.push({ address: parsed.address, cidr: parsed.prefix, version: parsed.version, status: IpAddressStatus.AVAILABLE, subnet: block }); } else { prepared.push({ address: parsed.address, cidr: parsed.prefix, version: parsed.version, status: IpAddressStatus.RESERVED, subnet: block }); } } if (prepared.length === 0) { throw new HttpError(400, "No addresses or CIDR blocks supplied", "EMPTY_IMPORT"); } const uniqueMap = new Map(); for (const item of prepared) { const key = `${item.address}/${item.cidr}`; if (!uniqueMap.has(key)) { uniqueMap.set(key, item); } } const values = [...uniqueMap.values()]; const existing = await prisma.ipAddressPool.findMany({ where: { OR: values.map((item) => ({ address: item.address, cidr: item.cidr })) }, select: { address: true, cidr: true } }); const existingKey = new Set(existing.map((item) => `${item.address}/${item.cidr}`)); const toInsert = values.filter((item) => !existingKey.has(`${item.address}/${item.cidr}`)); if (toInsert.length > 0) { await prisma.ipAddressPool.createMany({ data: toInsert.map((item) => ({ address: item.address, cidr: item.cidr, version: item.version, scope: input.scope ?? IpScope.PUBLIC, status: item.status, gateway: input.gateway, subnet: item.subnet, server: input.server, node_id: input.node_id, node_hostname: input.node_hostname, bridge: input.bridge, vlan_tag: input.vlan_tag, sdn_zone: input.sdn_zone, tags: baseTags, metadata, imported_by: input.imported_by })) }); } return { imported: toInsert.length, skipped_existing: values.length - toInsert.length, total_candidates: values.length }; } export async function assignIpToVm(input: AssignIpInput) { const vm = await fetchVmForNetwork(input.vm_id); const assignmentType = input.assignment_type ?? IpAssignmentType.ADDITIONAL; const metadata = input.metadata ? toPrismaJsonValue(input.metadata) : {}; const resolvedPolicy = await resolveAllocationPolicy({ tenant_id: vm.tenant_id, scope: input.scope, version: input.version, node_hostname: vm.node, bridge: null, vlan_tag: null, sdn_zone: null }); const where: Prisma.IpAddressPoolWhereInput = { status: IpAddressStatus.AVAILABLE }; if (input.ip_address_id) where.id = input.ip_address_id; if (input.address) where.address = input.address; if (input.scope) where.scope = input.scope; if (input.version) where.version = input.version; if (resolvedPolicy?.scope) where.scope = resolvedPolicy.scope; if (resolvedPolicy?.version) where.version = resolvedPolicy.version; if (resolvedPolicy?.node_hostname) where.node_hostname = resolvedPolicy.node_hostname; if (resolvedPolicy?.bridge) where.bridge = resolvedPolicy.bridge; if (typeof resolvedPolicy?.vlan_tag === "number") where.vlan_tag = resolvedPolicy.vlan_tag; if (resolvedPolicy?.sdn_zone) where.sdn_zone = resolvedPolicy.sdn_zone; const candidates = await prisma.ipAddressPool.findMany({ where, select: { id: true, address: true, cidr: true, version: true, scope: true, subnet: true, node_hostname: true, bridge: true, vlan_tag: true, sdn_zone: true, imported_at: true }, orderBy: [{ imported_at: "asc" }, { address: "asc" }], take: input.ip_address_id || input.address ? 5 : 3000 }); if (candidates.length === 0) { throw new HttpError(404, "No available IP address found", "IP_NOT_AVAILABLE"); } const reservedRanges = await prisma.ipReservedRange.findMany({ where: { is_active: true, OR: [{ tenant_id: vm.tenant_id }, { tenant_id: null }] }, select: { tenant_id: true, cidr: true, scope: true, version: true, node_hostname: true, bridge: true, vlan_tag: true, sdn_zone: true } }); const candidateFlags = candidates .map((candidate) => { const reservation = isCandidateReservedForOtherTenant(candidate, reservedRanges, vm.tenant_id); return { ...candidate, tenantReserved: reservation.tenantReserved, blocked: reservation.blocked }; }) .filter((candidate) => !(resolvedPolicy?.disallow_reserved_use ?? true ? candidate.blocked : false)); if (candidateFlags.length === 0) { throw new HttpError(409, "All candidate IPs are reserved by policy", "IP_RESERVED_BY_POLICY"); } const strategy = resolvedPolicy?.allocation_strategy ?? IpAllocationStrategy.BEST_FIT; const picked = pickBestFitCandidate(candidateFlags, strategy); if (!picked) { throw new HttpError(404, "No allocatable IP candidate found", "IP_NOT_AVAILABLE"); } await enforceTenantQuota({ tenant_id: vm.tenant_id, version: picked.version, policyEnforced: resolvedPolicy?.enforce_quota ?? true }); return prisma.$transaction(async (tx) => { const updatedIp = await tx.ipAddressPool.update({ where: { id: picked.id }, data: { status: IpAddressStatus.ASSIGNED, assigned_vm_id: vm.id, assigned_tenant_id: vm.tenant_id, assigned_at: new Date(), returned_at: null } }); const assignment = await tx.ipAssignment.create({ data: { ip_address_id: updatedIp.id, vm_id: vm.id, tenant_id: vm.tenant_id, assignment_type: assignmentType, interface_name: input.interface_name, notes: input.notes, metadata, assigned_by: input.actor_email }, include: { ip_address: true, vm: { select: { id: true, name: true, tenant_id: true } } } }); if (assignmentType === IpAssignmentType.PRIMARY) { await tx.virtualMachine.update({ where: { id: vm.id }, data: { ip_address: updatedIp.address } }); } return assignment; }); } export async function returnAssignedIp(input: ReturnIpInput) { let assignment = input.assignment_id ? await prisma.ipAssignment.findUnique({ where: { id: input.assignment_id }, include: { ip_address: true, vm: { select: { id: true, ip_address: true } } } }) : null; if (!assignment && input.ip_address_id) { assignment = await prisma.ipAssignment.findFirst({ where: { ip_address_id: input.ip_address_id, is_active: true }, include: { ip_address: true, vm: { select: { id: true, ip_address: true } } }, orderBy: { assigned_at: "desc" } }); } if (!assignment) { throw new HttpError(404, "Active IP assignment not found", "IP_ASSIGNMENT_NOT_FOUND"); } if (!assignment.is_active) { return assignment; } return prisma.$transaction(async (tx) => { const released = await tx.ipAssignment.update({ where: { id: assignment!.id }, data: { is_active: false, released_at: new Date() }, include: { ip_address: true, vm: { select: { id: true, ip_address: true } } } }); await tx.ipAddressPool.update({ where: { id: assignment!.ip_address_id }, data: { status: IpAddressStatus.AVAILABLE, assigned_vm_id: null, assigned_tenant_id: null, assigned_at: null, returned_at: new Date() } }); if ( assignment!.assignment_type === IpAssignmentType.PRIMARY && released.vm.ip_address && released.vm.ip_address === released.ip_address.address ) { await tx.virtualMachine.update({ where: { id: released.vm.id }, data: { ip_address: null } }); } return released; }); } export async function listIpAssignments(params?: { vm_id?: string; tenant_id?: string; active_only?: boolean }) { const where: Prisma.IpAssignmentWhereInput = {}; if (params?.vm_id) where.vm_id = params.vm_id; if (params?.tenant_id) where.tenant_id = params.tenant_id; if (params?.active_only) where.is_active = true; return prisma.ipAssignment.findMany({ where, include: { ip_address: true, vm: { select: { id: true, name: true, tenant_id: true, node: true } } }, orderBy: [{ is_active: "desc" }, { assigned_at: "desc" }] }); } export async function subnetUtilizationDashboard(input?: { scope?: IpScope; version?: IpVersion; node_hostname?: string; bridge?: string; vlan_tag?: number; tenant_id?: string; }) { const where: Prisma.IpAddressPoolWhereInput = {}; if (input?.scope) where.scope = input.scope; if (input?.version) where.version = input.version; if (input?.node_hostname) where.node_hostname = input.node_hostname; if (input?.bridge) where.bridge = input.bridge; if (typeof input?.vlan_tag === "number") where.vlan_tag = input.vlan_tag; if (input?.tenant_id) { where.OR = [{ assigned_tenant_id: input.tenant_id }, { status: IpAddressStatus.AVAILABLE }]; } const [ips, quotas, activeAssignments, reservedRanges] = await Promise.all([ prisma.ipAddressPool.findMany({ where, select: { id: true, address: true, cidr: true, subnet: true, scope: true, version: true, status: true, assigned_tenant_id: true, node_hostname: true, bridge: true, vlan_tag: true } }), prisma.tenantIpQuota.findMany({ where: input?.tenant_id ? { tenant_id: input.tenant_id } : undefined, include: { tenant: { select: { id: true, name: true } } } }), prisma.ipAssignment.findMany({ where: { is_active: true, ...(input?.tenant_id ? { tenant_id: input.tenant_id } : {}) }, include: { ip_address: { select: { version: true } } } }), prisma.ipReservedRange.findMany({ where: { is_active: true, ...(input?.tenant_id ? { OR: [{ tenant_id: input.tenant_id }, { tenant_id: null }] } : {}) } }) ]); const subnetMap = new Map< string, { subnet: string; scope: IpScope; version: IpVersion; node_hostname: string | null; bridge: string | null; vlan_tag: number | null; total: number; available: number; assigned: number; reserved: number; retired: number; } >(); for (const ip of ips) { const subnet = ip.subnet ?? `${ip.address}/${ip.cidr}`; const key = `${ip.scope}:${ip.version}:${ip.node_hostname ?? "-"}:${ip.bridge ?? "-"}:${ip.vlan_tag ?? "-"}:${subnet}`; if (!subnetMap.has(key)) { subnetMap.set(key, { subnet, scope: ip.scope, version: ip.version, node_hostname: ip.node_hostname, bridge: ip.bridge, vlan_tag: ip.vlan_tag, total: 0, available: 0, assigned: 0, reserved: 0, retired: 0 }); } const entry = subnetMap.get(key)!; entry.total += 1; if (ip.status === IpAddressStatus.AVAILABLE) entry.available += 1; if (ip.status === IpAddressStatus.ASSIGNED) entry.assigned += 1; if (ip.status === IpAddressStatus.RESERVED) entry.reserved += 1; if (ip.status === IpAddressStatus.RETIRED) entry.retired += 1; } const subnets = [...subnetMap.values()] .map((item) => { const utilization_pct = item.total > 0 ? Number(((item.assigned / item.total) * 100).toFixed(2)) : 0; const pressure_pct = item.total > 0 ? Number((((item.assigned + item.reserved) / item.total) * 100).toFixed(2)) : 0; return { ...item, utilization_pct, pressure_pct }; }) .sort((a, b) => b.utilization_pct - a.utilization_pct || a.subnet.localeCompare(b.subnet)); const assignmentSummary = activeAssignments.reduce( (acc, item) => { if (item.ip_address.version === IpVersion.IPV4) acc.ipv4 += 1; if (item.ip_address.version === IpVersion.IPV6) acc.ipv6 += 1; return acc; }, { total: activeAssignments.length, ipv4: 0, ipv6: 0 } ); return { subnets, quota_summary: quotas.map((quota) => ({ tenant_id: quota.tenant_id, tenant_name: quota.tenant.name, ipv4_limit: quota.ipv4_limit, ipv6_limit: quota.ipv6_limit, burst_allowed: quota.burst_allowed })), assignment_summary: assignmentSummary, reserved_range_count: reservedRanges.length }; } export async function upsertTenantIpQuota(input: UpsertTenantQuotaInput) { return prisma.tenantIpQuota.upsert({ where: { tenant_id: input.tenant_id }, update: { ipv4_limit: input.ipv4_limit ?? undefined, ipv6_limit: input.ipv6_limit ?? undefined, reserved_ipv4: input.reserved_ipv4, reserved_ipv6: input.reserved_ipv6, burst_allowed: input.burst_allowed, burst_ipv4_limit: input.burst_ipv4_limit ?? undefined, burst_ipv6_limit: input.burst_ipv6_limit ?? undefined, is_active: input.is_active, metadata: input.metadata ? toPrismaJsonValue(input.metadata) : undefined }, create: { tenant_id: input.tenant_id, ipv4_limit: input.ipv4_limit ?? null, ipv6_limit: input.ipv6_limit ?? null, reserved_ipv4: input.reserved_ipv4 ?? 0, reserved_ipv6: input.reserved_ipv6 ?? 0, burst_allowed: input.burst_allowed ?? false, burst_ipv4_limit: input.burst_ipv4_limit ?? null, burst_ipv6_limit: input.burst_ipv6_limit ?? null, is_active: input.is_active ?? true, metadata: input.metadata ? toPrismaJsonValue(input.metadata) : {}, created_by: input.created_by }, include: { tenant: { select: { id: true, name: true } } } }); } export async function listTenantIpQuotas(tenantId?: string) { return prisma.tenantIpQuota.findMany({ where: tenantId ? { tenant_id: tenantId } : undefined, include: { tenant: { select: { id: true, name: true, slug: true } } }, orderBy: [{ created_at: "desc" }] }); } export async function createIpReservedRange(input: CreateReservedRangeInput) { const parsed = parseCidr(input.cidr); return prisma.ipReservedRange.create({ data: { name: input.name, cidr: input.cidr, version: parsed.version, scope: input.scope ?? IpScope.PUBLIC, tenant_id: input.tenant_id, reason: input.reason, node_hostname: input.node_hostname, bridge: input.bridge, vlan_tag: input.vlan_tag, sdn_zone: input.sdn_zone, is_active: input.is_active ?? true, metadata: input.metadata ? toPrismaJsonValue(input.metadata) : {}, created_by: input.created_by } }); } export async function listIpReservedRanges() { return prisma.ipReservedRange.findMany({ include: { tenant: { select: { id: true, name: true } } }, orderBy: [{ is_active: "desc" }, { created_at: "desc" }] }); } export async function updateIpReservedRange(rangeId: string, input: Partial) { const parsed = input.cidr ? parseCidr(input.cidr) : null; return prisma.ipReservedRange.update({ where: { id: rangeId }, data: { name: input.name, cidr: input.cidr, version: parsed?.version, scope: input.scope, tenant_id: input.tenant_id, reason: input.reason, node_hostname: input.node_hostname, bridge: input.bridge, vlan_tag: input.vlan_tag, sdn_zone: input.sdn_zone, is_active: input.is_active, metadata: input.metadata ? toPrismaJsonValue(input.metadata) : undefined } }); } export async function upsertIpPoolPolicy(input: UpsertIpPoolPolicyInput) { if (input.policy_id) { return prisma.ipPoolPolicy.update({ where: { id: input.policy_id }, data: { name: input.name, tenant_id: input.tenant_id, scope: input.scope, version: input.version, node_hostname: input.node_hostname, bridge: input.bridge, vlan_tag: input.vlan_tag, sdn_zone: input.sdn_zone, allocation_strategy: input.allocation_strategy, enforce_quota: input.enforce_quota, disallow_reserved_use: input.disallow_reserved_use, is_active: input.is_active, priority: input.priority, metadata: input.metadata ? toPrismaJsonValue(input.metadata) : undefined } }); } return prisma.ipPoolPolicy.create({ data: { name: input.name, tenant_id: input.tenant_id, scope: input.scope, version: input.version, node_hostname: input.node_hostname, bridge: input.bridge, vlan_tag: input.vlan_tag, sdn_zone: input.sdn_zone, allocation_strategy: input.allocation_strategy ?? IpAllocationStrategy.BEST_FIT, enforce_quota: input.enforce_quota ?? true, disallow_reserved_use: input.disallow_reserved_use ?? true, is_active: input.is_active ?? true, priority: input.priority ?? 100, metadata: input.metadata ? toPrismaJsonValue(input.metadata) : {}, created_by: input.created_by } }); } export async function listIpPoolPolicies() { return prisma.ipPoolPolicy.findMany({ include: { tenant: { select: { id: true, name: true } } }, orderBy: [{ priority: "asc" }, { created_at: "asc" }] }); } export async function createPrivateNetwork(input: CreatePrivateNetworkInput) { parseCidr(input.cidr); const slug = input.slug && input.slug.trim().length > 0 ? normalizeSlug(input.slug) : normalizeSlug(input.name); return prisma.privateNetwork.create({ data: { name: input.name, slug, network_type: input.network_type ?? PrivateNetworkType.VLAN, cidr: input.cidr, gateway: input.gateway, bridge: input.bridge, vlan_tag: input.vlan_tag, sdn_zone: input.sdn_zone, server: input.server, node_hostname: input.node_hostname, metadata: input.metadata ? toPrismaJsonValue(input.metadata) : {}, created_by: input.created_by } }); } export async function listPrivateNetworks() { return prisma.privateNetwork.findMany({ include: { attachments: { where: { status: PrivateNetworkAttachmentStatus.ATTACHED }, select: { id: true, vm_id: true, interface_name: true, attached_at: true } } }, orderBy: [{ created_at: "desc" }] }); } export async function attachPrivateNetwork(input: AttachPrivateNetworkInput) { const [network, vm] = await Promise.all([ prisma.privateNetwork.findUnique({ where: { id: input.network_id } }), fetchVmForNetwork(input.vm_id) ]); if (!network) { throw new HttpError(404, "Private network not found", "PRIVATE_NETWORK_NOT_FOUND"); } const interfaceName = input.interface_name ?? "net1"; const runtime = vmRuntime(vm.type); const bridge = network.bridge ?? (typeof network.vlan_tag === "number" ? `vmbr${network.vlan_tag}` : "vmbr0"); const upid = await reconfigureVmNetwork(vm.node, vm.vmid, runtime, { interface_name: interfaceName, bridge, vlan_tag: network.vlan_tag ?? undefined, ip_mode: input.requested_ip ? "static" : "dhcp", ip_cidr: input.requested_ip, gateway: input.requested_ip ? network.gateway ?? undefined : undefined }); const existing = await prisma.privateNetworkAttachment.findFirst({ where: { network_id: network.id, vm_id: vm.id, interface_name: interfaceName } }); if (existing) { return prisma.privateNetworkAttachment.update({ where: { id: existing.id }, data: { tenant_id: vm.tenant_id, requested_ip: input.requested_ip, status: PrivateNetworkAttachmentStatus.ATTACHED, detached_at: null, attached_by: input.actor_email, attached_at: new Date(), metadata: toPrismaJsonValue({ ...(input.metadata ?? {}), proxmox_upid: upid }) }, include: { network: true, vm: { select: { id: true, name: true, tenant_id: true, node: true } } } }); } return prisma.privateNetworkAttachment.create({ data: { network_id: network.id, vm_id: vm.id, tenant_id: vm.tenant_id, interface_name: interfaceName, requested_ip: input.requested_ip, status: PrivateNetworkAttachmentStatus.ATTACHED, attached_by: input.actor_email, metadata: toPrismaJsonValue({ ...(input.metadata ?? {}), proxmox_upid: upid }) }, include: { network: true, vm: { select: { id: true, name: true, tenant_id: true, node: true } } } }); } export async function detachPrivateNetwork(input: DetachPrivateNetworkInput) { const attachment = await prisma.privateNetworkAttachment.findUnique({ where: { id: input.attachment_id }, include: { vm: { select: { id: true, name: true, tenant_id: true, vmid: true, node: true, type: true } } } }); if (!attachment) { throw new HttpError(404, "Private network attachment not found", "PRIVATE_NETWORK_ATTACHMENT_NOT_FOUND"); } if (attachment.status === PrivateNetworkAttachmentStatus.DETACHED) { return attachment; } const runtime = vmRuntime(attachment.vm.type); const interfaceName = attachment.interface_name ?? "net1"; const upid = await updateVmConfiguration(attachment.vm.node, attachment.vm.vmid, runtime, { delete: interfaceName }); return prisma.privateNetworkAttachment.update({ where: { id: attachment.id }, data: { status: PrivateNetworkAttachmentStatus.DETACHED, detached_at: new Date(), metadata: toPrismaJsonValue({ ...(attachment.metadata as Record), detached_by: input.actor_email, detach_upid: upid }) }, include: { network: true, vm: { select: { id: true, name: true, tenant_id: true, node: true } } } }); }