Files
MikrotikManager/backend/src/services/gre-native-mutation.ts
T

56 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod"
import type { ServerRow } from "../modules/servers/repository/servers-repository.js"
import { getServerRowById } from "../modules/servers/repository/servers-repository.js"
import { fingerprint } from "../modules/mcp/security.js"
import { greNativeFieldsSchema } from "./gre-mutation-input.js"
import { assertGreDeleteDependencies, greMutationProblem, greMutationProof, greTarget, readGreMutationState, type GreMutationState } from "./gre-mutation-state.js"
import { executeGreMutationOps, recordGreMutationRevision } from "./gre-mutation.js"
import { withApplicationMutationLock, withConfigMutationLock } from "./config-mutation-lock.js"
import { observedBoolean } from "./ospf-observed-values.js"
import { MikrotikClient } from "./mikrotik.js"
import type { RosWriteOp } from "./entity-snapshots.js"
const id=z.string().regex(/^\*[A-Za-z0-9]+$/).max(100)
export const greNativeMutationSchema=z.discriminatedUnion("operation",[
z.object({operation:z.literal("update"),id,fields:greNativeFieldsSchema.refine(fields=>Object.keys(fields).length>0,"Нет параметров изменения")}).strict(),
z.object({operation:z.literal("delete"),id}).strict(),
])
export type GreNativeMutation=z.infer<typeof greNativeMutationSchema>
export const greNativeSelector=(input:GreNativeMutation)=>({operation:"delete" as const,rosId:input.id})
export async function readGreNativeMutationState(server:ServerRow,input:GreNativeMutation){return readGreMutationState(server,greNativeSelector(input),{identityOnly:true})}
/** Native update/delete keeps its original one-interface meaning. In particular,
* deletion never acquires implicit permission to remove configured addresses. */
export function planGreNativeMutation(input:GreNativeMutation,before:GreMutationState):RosWriteOp {
const target=greTarget(greNativeSelector(input),before)
if(!target)throw greMutationProblem("GRE с этим native ID не найден")
if([target.dynamic,target.builtin,target.default].some(value=>observedBoolean(value)===true))throw greMutationProblem("Системный/динамический GRE нельзя изменять")
const rename=input.operation==="update"&&input.fields.name!==undefined&&input.fields.name!==target.name
if(input.operation==="delete"||rename){
assertGreDeleteDependencies(before,target)
if([...before.ipv4,...before.ipv6].some(row=>[target.name,target[".id"]].includes(row.interface)&&!(observedBoolean(row.dynamic)===true&&/^fe80:/i.test(row.address))))throw greMutationProblem("GRE используется адресом; сначала измените адреса отдельным планом")
}
if(rename&&input.operation==="update"&&(before.gre.some(row=>row[".id"]!==target[".id"]&&row.name===input.fields.name)||before.dependencies["/interface"].some(row=>row[".id"]!==target[".id"]&&row.name===input.fields.name)))throw greMutationProblem("Новое имя GRE уже используется")
if(input.operation==="delete")return{op:"delete",path:"/interface/gre/"+input.id}
return{op:"patch",path:"/interface/gre/"+input.id,body:Object.fromEntries(Object.entries(input.fields).map(([key,value])=>[key,String(value)]))}
}
export async function mutateGreNativeTunnel(server:ServerRow,raw:GreNativeMutation,expectedBefore?:string,options:{recordRevision?:boolean}={}){
const input=greNativeMutationSchema.parse(raw)
return withApplicationMutationLock(()=>withConfigMutationLock([server.id],async()=>{
const fresh=await getServerRowById(server.id)
if(!fresh?.enabled||fingerprint(server)!==fingerprint(fresh))throw greMutationProblem("Источник GRE изменился")
const before=await readGreNativeMutationState(fresh,input)
if(expectedBefore&&fingerprint(greMutationProof(before))!==expectedBefore)throw greMutationProblem("GRE или его зависимости изменились после подготовки")
const op=planGreNativeMutation(input,before),client=MikrotikClient.fromServer(fresh)
const receipt=await executeGreMutationOps(greNativeSelector(input),before,[op],{read:()=>readGreNativeMutationState(fresh,input),validate:state=>{planGreNativeMutation(input,state)},command:op=>{
const path="/interface/gre/"+encodeURIComponent(input.id)
if(op.op==="delete")return client.delete(path)
if(op.op==="patch")return client.patch(path,op.body)
throw greMutationProblem("Недопустимая native-команда GRE")
}})
const revision=receipt.verified&&options.recordRevision!==false?await recordGreMutationRevision(fresh):{revisionRecorded:false,revisionError:null}
return{verified:receipt.verified,acknowledged:receipt.acknowledgedCommands>0,commandCount:1,completedCommands:receipt.completedCommands,acknowledgedCommands:receipt.acknowledgedCommands,afterFingerprint:fingerprint(greMutationProof(receipt.after)),...revision,forwardingVerified:false}
}))
}