0

Apply config

by
Published today

Declaratively apply configuration at a RESTCONF data path: read the current config, diff it against the intended JSON, and merge (PATCH) only when they differ. dry_run defaults to true and returns the diff without changing anything.

Script cisco_iosxe Verified

The script

Submitted by hugo989 Typescript (fetch-only)
Verified 2 hours ago
1
//native
2

3
type Change = { path: string; current: any; intended: any }
4

5
// Match a member by its full name, or by its local name when one side carries
6
// a module prefix and the other doesn't (RFC 7951 qualifies only the top level
7
// and module changes).
8
function lookup(obj: { [key: string]: any }, key: string) {
9
  if (key in obj) return obj[key]
10
  const local = key.split(":").pop()
11
  const match = Object.keys(obj).find((k) => k.split(":").pop() === local)
12
  return match === undefined ? undefined : obj[match]
13
}
14

15
// RFC 7951 encodes 64-bit integers and decimals as strings, so "1500" and 1500
16
// are the same value.
17
function scalarEqual(a: any, b: any) {
18
  return a === b || (a != null && b != null && String(a) === String(b))
19
}
20

21
// Collects every place where `intended` is not already present in `current`.
22
// Objects compare member by member; a list entry is satisfied when some current
23
// entry contains all of its members. Members of `current` that `intended` does
24
// not mention are ignored, as a PATCH merge leaves them untouched.
25
function diff(intended: any, current: any, at: string, out: Change[]) {
26
  if (Array.isArray(intended)) {
27
    const entries = Array.isArray(current) ? current : []
28
    intended.forEach((item, i) => {
29
      if (!entries.some((entry) => diff(item, entry, "", []).length === 0)) {
30
        out.push({ path: `${at}[${i}]`, current: null, intended: item })
31
      }
32
    })
33
  } else if (intended !== null && typeof intended === "object") {
34
    // A GET on a list entry (interface/Loopback=1) returns a one-entry array.
35
    if (Array.isArray(current) && current.length === 1) {
36
      current = current[0]
37
    }
38
    if (
39
      current === null ||
40
      typeof current !== "object" ||
41
      Array.isArray(current)
42
    ) {
43
      out.push({ path: at || "/", current: current ?? null, intended })
44
    } else {
45
      for (const [key, value] of Object.entries(intended)) {
46
        diff(value, lookup(current, key), `${at}/${key}`, out)
47
      }
48
    }
49
  } else if (!scalarEqual(intended, current)) {
50
    out.push({ path: at || "/", current: current ?? null, intended })
51
  }
52
  return out
53
}
54

55
/**
56
 * Apply config
57
 * Declaratively apply configuration at a RESTCONF data path: read the current config there, diff it against the intended JSON, and merge (PATCH) only when they differ. intended is the RFC 7951 body wrapped in the target node, as a GET of path returns it, e.g. {"Cisco-IOS-XE-native:Loopback": {"name": 100, "description": "x"}} for path Cisco-IOS-XE-native:native/interface/Loopback=100. With dry_run (the default) nothing is changed and the diff is returned.
58
 */
59
export async function main(
60
  auth: RT.CiscoIosxe,
61
  path: string,
62
  intended: { [key: string]: any },
63
  dry_run: boolean = true
64
) {
65
  const url = new URL(
66
    `${auth.base_url.replace(/\/+$/, "")}/restconf/data/${path.replace(/^\/+/, "")}`
67
  )
68
  const authorization = `Basic ${btoa(`${auth.username}:${auth.password}`)}`
69

70
  const currentResponse = await fetch(url, {
71
    headers: {
72
      Authorization: authorization,
73
      Accept: "application/yang-data+json",
74
    },
75
  })
76
  let current: any = null
77
  if (currentResponse.ok) {
78
    current = currentResponse.status === 204 ? {} : await currentResponse.json()
79
  } else if (currentResponse.status !== 404) {
80
    throw new Error(`${currentResponse.status} ${await currentResponse.text()}`)
81
  }
82

83
  const changes = diff(intended, current ?? {}, "", [])
84
  if (changes.length === 0 || dry_run !== false) {
85
    return { changed: changes.length > 0, diff: changes, applied: false }
86
  }
87

88
  // PATCH merges into an existing resource; when the path holds no data yet,
89
  // PUT creates it (RFC 8040 sections 4.5 and 4.6.1). Leaves that already match
90
  // are rewritten with the same value, which changes nothing.
91
  const method = current === null ? "PUT" : "PATCH"
92
  const response = await fetch(url, {
93
    method,
94
    headers: {
95
      Authorization: authorization,
96
      "Content-Type": "application/yang-data+json",
97
      Accept: "application/yang-data+json",
98
    },
99
    body: JSON.stringify(intended),
100
  })
101

102
  if (!response.ok) {
103
    throw new Error(`${response.status} ${await response.text()}`)
104
  }
105

106
  return { changed: true, diff: changes, applied: true, method }
107
}
108