Skip to content
academia.sh

Lesson 12 / 16

Programmable Interfaces

When the same change is applied through three request forms, the append form changes state on a second application in all forty of forty devices; when nine devices with no response are retried, append breaks five of them, the two idempotent forms break neither, but full replace erases nine devices' out-of-model field.

Contents

The previous lesson measured what a configuration model can say, and assumed the model stays where it is written. For a model to be of any use, it has to reach the device: a request goes out from somewhere, the device receives it, applies it, and returns a response. This path has its own rules.

The most important one comes down to a single question: what does sending the same request twice do? The question looks theoretical, but it grows out of operations’ most common situation — the response never arrives, and the operator cannot know whether the request was applied. What this lesson measures is not the interface’s speed or elegance, it is the answer to this question.

What the Interface Provides

The oldest way to send configuration to a device is opening a shell session and typing commands. This path has three flaws from automation’s point of view, and all three come from the same root: the output is formatted for a human.

A script can only learn whether a command succeeded by parsing the output text. If the text’s format changes, the script silently reads it wrong. What comes back on failure is not an error code, it is a sentence. And there is no concept of multiple commands applying together — if the third command is rejected, the first two are already in effect.

A programmable interface closes all three at once. Both the request and the response are structured according to the previous lesson’s model; fields arrive by name, and there is no text to parse. An error returns as a processable structure: it states which field violated which constraint. And the change can be wrapped in a transaction boundary — the request is first accumulated as a candidate configuration, validated, then committed in a single step.

# taught request/response transcript, not run

request
  target  : device/34
  op      : merge
  body    : { priority: 3 , rule: [ base , template_rule ] }

response
  status  : accepted
  changed : [ priority , rule ]
  txn     : 7f

The response’s changed list is the interface’s most useful field: it tells you what actually changed on the device. If the list returns empty, the request was applied but changed nothing — the device was already in the desired state.

Three Request Forms

The same change can be requested in three separate forms, and the three do not leave the same result on the device.

Append. The request says “add this rule to the list.” The device puts one more row in the list. Sent twice, the list gets two rows.

Merge. The request says “these fields have this value.” The device matches fields by key and overwrites them; it does not repeat the same entry in a list. Sent twice, the second does nothing.

Full replace. The request gives the entire configuration. The device discards what it has and sets up exactly what the request says. Sent twice, the second does nothing — but the first sending has also erased every field not present in the request.

This is what makes an interface idempotent: if applying the request once and applying it twice leave the same state, the interface is idempotent. Idempotence is not a property of speed or correctness; it is a property of retryability.

Response Loss and Retry

Why idempotence is an operational problem shows up in a single setup. The request is sent, the device receives and applies it, but the response is lost on the way back. The operator has no information: the request may never have reached the device, or it may have been applied. The two are indistinguishable from outside.

In this situation, the only thing to do is retry. On an idempotent interface, retrying is free — if the request was applied, the second one does nothing, and if it was not, it applies it. On an interface that is not idempotent, retrying breaks the devices that already succeeded. And what breaks are exactly the devices where the request worked.

The assumptions the measurement rests on:

  • AC13 — The forty-device subject set is the course’s constant. Each device’s state before the change is derived from its attributes: interface, address, and a base rule; on a special device, additionally a local rule the model does not carry.
  • AC14 — The three request forms carry the same change: adding a rule and setting priority. The difference is only in the request’s form, not its content.
  • AC15 — The idempotence criterion is a comparison of states: if applying the request once and applying it twice leave the same state, the interface is idempotent. The measurement does not look at the interface’s own claim.
  • AC16 — Response loss is the setup’s parameter: one in five devices gets no response, and roughly half of those with no response have actually had the request applied. This is not a measurement; if the rate changes, every row scales in the same direction.
  • AC17 — The retry sends the same request again to every device that got no response. The operator cannot know which ones were applied; if they could, there would be no need to retry.
  • AC18 — The template is written for a device class, and the report is built on the interactive class; a special device whose class does not match deviates, and a deviation in another class never appears in the report.

The Measurement

"""Programmable interface: what sending the same request twice does.

Part 1 - three request forms, the difference a second application leaves.
Part 2 - when the response is lost, what a retry breaks.
Part 3 - the tail: what falls outside the template and what the report cannot see.
"""
SEED = 20260812
EXTRA = "template_rule"
FORMS = ("append", "merge", "full replace")


def make_rng(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def subjects(count=40, seed=SEED):
    r, out = make_rng(seed), []
    for i in range(count):
        out.append({"no": i + 1, "capacity": 20 + r(81),
                    "latency": 5 + r(45),
                    "class": ("interactive", "batch", "standby")[r(3)],
                    "special": r(9) == 0})
    return out


def state(o):
    """The device's configuration before the change."""
    d = {"interface": "main", "address": f"a{o['no']}", "rule": ["base"]}
    if o["special"]:
        d["local_rule"] = f"k{o['no']}"
    return d


def request(d, form):
    """The same change is applied through three separate request forms."""
    y = {k: (list(v) if isinstance(v, list) else v) for k, v in d.items()}
    if form == "append":
        y["rule"] = y["rule"] + [EXTRA]
    elif form == "merge":
        y["rule"] = [k for k in y["rule"] if k != EXTRA] + [EXTRA]
    elif form == "full replace":
        y = {"interface": "main", "address": d["address"], "rule": ["base", EXTRA]}
    y["priority"] = "3"
    return y


def apply_template(subj, template_class):
    """A single template is applied to forty devices; special devices deviate."""
    matched = deviated = hidden = 0
    for o in subj:
        if o["class"] == template_class or not o["special"]:
            matched += 1
        else:
            deviated += 1
            if o["class"] != "interactive":
                hidden += 1
    return matched, deviated, hidden


subj = subjects()
print(f"subjects {len(subj)} | devices carrying an out-of-model field "
      f"{sum(o['special'] for o in subj)} | request forms {len(FORMS)}")
print()
print(f"{'request form':<14s} {'second application changes':>27s} {'idempotent':>10s} "
      f"{'out-of-model field erased':>26s}")
for b in FORMS:
    changed = erased = 0
    for o in subj:
        d = state(o)
        first = request(d, b)
        second = request(first, b)
        if second != first:
            changed += 1
        if "local_rule" in d and "local_rule" not in first:
            erased += 1
    print(f"{b:<14s} {changed:27d} {'no' if changed else 'yes':>10s} "
          f"{erased:26d}")

print()
r = make_rng(SEED + 11)
no_response, actually_applied = [], set()
for o in subj:
    if r(5) == 0:
        no_response.append(o["no"])
        if r(2) == 0:
            actually_applied.add(o["no"])
print(f"devices with no response {len(no_response)} | of these, request actually "
      f"applied {len(actually_applied)}")
print()
print(f"{'request form':<14s} {'retried':>7s} {'reached correct state':>22s} "
      f"{'broken':>7s}")
for b in FORMS:
    correct = broken = 0
    for no in no_response:
        o = subj[no - 1]
        d = state(o)
        target = request(d, b)
        before = target if no in actually_applied else d
        after = request(before, b)
        if after == target:
            correct += 1
        else:
            broken += 1
    print(f"{b:<14s} {len(no_response):7d} {correct:22d} {broken:7d}")

print()
print(f"{'template class':<15s} {'matched':>7s} {'deviated':>8s} "
      f"{'hidden from report':>19s} {'accepted template, carries out-of-model field':>47s}")
for c in ("interactive", "batch", "standby"):
    m, d, h = apply_template(subj, c)
    risky = sum(1 for o in subj if o["special"] and o["class"] == c)
    print(f"{c:<15s} {m:7d} {d:8d} {h:19d} {risky:47d}")
subjects 40 | devices carrying an out-of-model field 9 | request forms 3

request form    second application changes idempotent  out-of-model field erased
append                                  40         no                          0
merge                                    0        yes                          0
full replace                             0        yes                          9

devices with no response 9 | of these, request actually applied 5

request form   retried  reached correct state  broken
append               9                      4       5
merge                9                      9       0
full replace         9                      9       0

template class  matched deviated  hidden from report   accepted template, carries out-of-model field
interactive          34        6                   6                                               3
batch                34        6                   3                                               3
standby              34        6                   3                                               3

The Measure of Idempotence

The top table separates the three forms. In the append form, the second application changes state in all forty of forty devices; the interface is not idempotent. In the merge and full replace forms, the second application changes nothing on any device; both are idempotent.

All forty of forty is notable. A violation of idempotence is not a minority event: in the append form, every device breaks on the second application, because the cause of breaking is not a device attribute, it is the request’s form. This is the opposite of the other tails measured in this course — here there is no tail, the lever misserves all forty of forty the same way.

The right-hand column gives the second distinction, and shows that idempotence alone is not enough. Full replace is idempotent, but it erases 9 devices’ out-of-model field. Since the request does not carry that field, the device treats it not as missing but as extra, and discards it. The previous lesson’s nine local_rule fields vanish here — and the interface reports no fault at all, because this is exactly what the requested operation is defined to do.

So idempotence and safety are two separate properties. An interface that is not idempotent breaks on retry; an interface that is idempotent but uses full replace erases on the first attempt. The form standing at the intersection of the two is merge: it is both retryable and preserves the fields it does not touch.

What Retrying Costs

The middle table runs the setup. 9 of the forty devices get no response, and 5 of those actually had the request applied. The operator has no information to make this distinction; all nine of the nine have to be retried.

In the append form, devices reaching the correct state after retry are 4, broken 5. The five broken devices are exactly the ones where the request worked the first time. The shape of this result is the worst possible from an operational standpoint: the procedure does damage exactly where it succeeded, and the damage scales with the success.

In the merge and full replace forms, all nine of the nine devices reach the correct state, broken 0. This is exactly what idempotence buys, and nothing more: it makes retrying safe to do without thinking.

In a set of forty subjects, the smallest measurable difference is 1/40 = 0.025. Five broken devices are a share of 0.125, comfortably inside the band.

What Idempotence Does Not Protect Against

Idempotence protects the operator against their own repetition. There is a situation it does not protect against, and it arises from the interface’s read side.

A script’s typical loop is three steps: read the device’s state, compute the change on top of it, write the result. Reading is idempotent, writing can be idempotent; but the three together are not a single unit. If two operators run this loop on the same device at the same time, the second writes after the first has read, and overwrites the first’s write without ever seeing it. The result is not the sum of two changes, it is the second one’s result alone; and no request returns an error, because each request is valid on its own.

Idempotence does nothing here. Sending the same request twice is not the problem; the problem is two different requests interleaving. The interface has two tools against this. The first is the transaction boundary: while the change accumulates in the candidate configuration, the device holds a lock, and the second operator’s write is rejected. The second is a stamp: every configuration gets a stamp, the request says “I saw this stamp, I am writing on top of it,” and if the stamp has changed, the request is rejected. Neither solves the problem, both make it visible — a rejected request is not an error, it is a notification.

Surfaces the Interface Opens and Their Narrowing

A programmable interface is a path that can change a device’s configuration; by definition, this makes it a surface. The counted surfaces and their narrowing follow.

First surface — the management path itself. If the interface is open, the device’s configuration can be changed over the network. Narrowing: the interface connects to a separate management network and is unreachable from the path data traffic takes; access is restricted by source address; read authority and write authority are granted separately. These three narrowings do not substitute for one another, they stack.

Second surface — where the credential sits. A script uses an identity to connect to the device, and that identity sits somewhere. Credentials, keys, and access tokens are not written in any example in this course. Narrowing: the value does not sit inside the script, the template, or the configuration repository; the script carries only a reference and gets the value from its runtime environment.

Third surface — the field full replace erases. In the measurement, 9 devices lose a field the request does not carry. This is not an attack, it is the interface’s defined behavior, and that is exactly why it is dangerous — it produces no warning. Narrowing: full replace is used only when the entire configuration is genuinely known; when it is not, merge is used. Devices carrying an out-of-model field are listed in advance, and the operation is halted when full replace’s scope intersects that list.

Fourth surface — a change that cuts off its own access. A request that shuts down the management interface itself, changes the management network’s address, or filters access makes the device unreachable the moment it is applied. Such a request’s command is not written in runnable form in this course; what can be said is that it is irreversible. Narrowing: the change is first written as a candidate configuration, its outcome is computed with a dry run, then it is committed with a rollback timer — if the operator does not confirm within a set period, the device automatically reverts to the previous configuration. The timer is the only tool left in the hands of an operator who has lost access.

Summary

  • A programmable interface provides structured requests and responses, processable errors, and a transaction boundary; it closes all three flaws of a script parsing shell output at once.
  • The append form is not idempotent: a second application changes state in 40 of forty devices. Merge and full replace are idempotent, 0 in both.
  • 5 of the 9 devices with no response actually had the request applied; on retry, the append form breaks these 5, the two idempotent forms break 0.
  • Idempotence and safety are separate: full replace is idempotent but erases 9 devices’ out-of-model field on the first application, and reports no fault.
  • On the template side, matched devices are 34, deviated 6, hidden from the report 6 / 3 / 3; 3 of the devices accepting the template carry an out-of-model field, and they are exactly the ones that would lose it under full replace.

Next Step

This lesson measured what a single request leaves on a single device. In operations, a request is never singular: a script connects to forty devices in sequence, applies the same template to each, and writes a report at the end. The next lesson measures that script itself — a bulk change precomputed with a dry run, spread in batches, and rolled back. And it is there that we answer the real question: when the template is applied, how many devices fall outside it, and how many of those devices appear in the report that gets written.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close