56 lines
4.8 KiB
TypeScript
56 lines
4.8 KiB
TypeScript
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}
|
||
}))
|
||
}
|