Edits history of script submission #23973 for ' Apply config (cisco_nxos)'

  • bunnative
    One script reply has been approved by the moderators
    Ap­pro­ved
    //native
    
    /**
     * Apply config
     * Declaratively apply configuration lines: compare the intended lines (indented like `show running-config`, two spaces per level) with the running configuration, return the lines missing from it with their parent blocks, and push only those. With dry_run (the default) nothing is changed. Set checkpoint_name to create a named checkpoint before applying, so the change can be undone with Rollback to checkpoint; NX-OS refuses an existing name, so use one per run (e.g. with the job ID). Lines must match the switch's own output: negations and default lines (no shutdown) never appear there, so they are always reported as changes.
     */
    export async function main(
      auth: RT.CiscoNxos,
      config: string,
      dry_run: boolean = true,
      checkpoint_name: string | undefined
    ) {
      const intended = parseConfig(config ?? "")
      if (intended.length === 0) {
        throw new Error("config must contain at least one configuration line")
      }
      // NX-API splits input on ';', so a line containing one would be cut in two.
      if (/;/.test(config)) {
        throw new Error("config lines must not contain ';'")
      }
      const checkpoint = checkpoint_name?.trim() || undefined
      if (checkpoint && /[\s;|]/.test(checkpoint)) {
        throw new Error("checkpoint_name must not contain spaces, ';' or '|'")
      }
    
      const [running] = await nxapi(auth, "cli_show_ascii", ["show running-config"])
      const missing: DiffLine[] = []
      diffNodes(intended, parseConfig(String(running?.body ?? "")), 0, missing)
    
      const diff = missing.map((l) => `${"  ".repeat(l.depth)}${l.text}`)
      // Sent in order: each changed block after its parent lines, with no 'exit'.
      // NX-OS returns to the parent mode when a line is not valid in the current
      // sub-mode, the command shape Ansible cisco.nxos and NAPALM send.
      const commands = missing.map((l) => l.text)
    
      if (missing.length === 0 || dry_run !== false) {
        return {
          changed: missing.length > 0,
          dry_run: dry_run !== false,
          diff,
          commands,
          applied: false,
          checkpoint: null,
        }
      }
    
      if (checkpoint) {
        await nxapi(auth, "cli_show_ascii", [`checkpoint ${checkpoint}`])
      }
      await nxapi(
        auth,
        "cli_conf",
        commands,
        checkpoint
          ? `Configuration may be partially applied; roll back with checkpoint "${checkpoint}".`
          : "Configuration may be partially applied."
      )
    
      return {
        changed: true,
        dry_run: false,
        diff,
        commands,
        applied: true,
        checkpoint: checkpoint ?? null,
      }
    }
    
    type ConfigNode = { text: string; children: ConfigNode[] }
    type DiffLine = { text: string; depth: number }
    
    // Builds a tree from indentation, ignoring blank lines and "!" comments
    function parseConfig(text: string): ConfigNode[] {
      const root: ConfigNode[] = []
      const stack: { indent: number; children: ConfigNode[] }[] = [
        { indent: -1, children: root },
      ]
      for (const raw of text.split(/\r?\n/)) {
        const line = raw.replace(/\t/g, "  ").replace(/\s+$/, "")
        const trimmed = line.trim()
        if (trimmed === "" || trimmed.startsWith("!")) continue
        const indent = line.length - line.trimStart().length
        while (stack[stack.length - 1].indent >= indent) stack.pop()
        const node: ConfigNode = {
          text: trimmed.replace(/\s+/g, " "),
          children: [],
        }
        stack[stack.length - 1].children.push(node)
        stack.push({ indent, children: node.children })
      }
      return root
    }
    
    // Collects intended lines absent from the running config, each preceded by its parents
    function diffNodes(
      intended: ConfigNode[],
      running: ConfigNode[],
      depth: number,
      out: DiffLine[]
    ) {
      for (const node of intended) {
        const match = running.find((r) => r.text === node.text)
        if (!match) {
          emitAll(node, depth, out)
          continue
        }
        const sub: DiffLine[] = []
        diffNodes(node.children, match.children, depth + 1, sub)
        if (sub.length > 0) {
          out.push({ text: node.text, depth })
          out.push(...sub)
        }
      }
    }
    
    function emitAll(node: ConfigNode, depth: number, out: DiffLine[]) {
      out.push({ text: node.text, depth })
      for (const child of node.children) emitAll(child, depth + 1, out)
    }
    
    type NxapiOutput = {
      input?: string
      body?: any
      code: string
      msg?: string
      clierror?: string
    }
    
    async function nxapi(
      auth: RT.CiscoNxos,
      type: "cli_show" | "cli_show_ascii" | "cli_conf",
      commands: string[],
      hint?: string
    ): Promise<NxapiOutput[]> {
      const response = await fetch(new URL("/ins", auth.base_url), {
        method: "POST",
        headers: {
          Authorization: `Basic ${btoa(`${auth.username}:${auth.password}`)}`,
          "Content-Type": "application/json",
          Accept: "application/json",
        },
        // Keep "type" before "input": some NX-OS releases misparse the payload otherwise
        body: JSON.stringify({
          ins_api: {
            version: "1.0",
            type,
            chunk: "0",
            sid: null,
            input: commands.join(" ;"),
            output_format: "json",
          },
        }),
      })
    
      const suffix = hint ? ` (${hint})` : ""
      if (!response.ok) {
        throw new Error(`${response.status} ${await response.text()}${suffix}`)
      }
      const text = await response.text()
      if (text.trimStart().startsWith("<")) {
        // NX-API falls back to XML when the output exceeds 1 MB
        throw new Error(
          `NX-API returned XML instead of JSON (outputs over 1 MB come back as XML): ${text.slice(0, 500)}`
        )
      }
    
      const data = JSON.parse(text)
      const raw = data?.ins_api?.outputs?.output
      const outputs: NxapiOutput[] =
        raw == null ? [] : Array.isArray(raw) ? raw : [raw]
      for (const o of outputs) {
        if (String(o.code) !== "200") {
          throw new Error(
            `${o.code} ${o.input ?? ""}: ${o.msg ?? ""} ${o.clierror ?? ""}`.trim() +
              suffix
          )
        }
      }
      return outputs
    }
    

    Submitted by hugo989 3 hours ago