docs: document generic-vs-device-specific design principle
Makes explicit a rule that's been applied ad hoc: matching/comparison/ orchestration logic that's identical across every driver of a device-type belongs as a concrete method on the abstract base class; only actual device communication (REST/CLI/payload format) belongs in the concrete vendor driver as an implementation of an abstract method. Uses the upcoming FirewallDriver diff/apply mechanism as the worked example.
This commit is contained in:
@@ -22,6 +22,50 @@ NAPALM's `NetworkDriver` defines a common interface for all network devices. In
|
||||
|
||||
`napalm-device-types` sits in between: it adds one well-typed layer of abstract methods per device category, so every driver for the same category exposes the same interface.
|
||||
|
||||
## Design principle: generic vs. device-specific logic
|
||||
|
||||
When adding behavior to a device-type base class, split it along one line: **would
|
||||
this exact logic work unchanged for a different vendor's driver of the same
|
||||
device-type, if that driver only implemented the same abstract methods?**
|
||||
|
||||
- If yes, it's generic — implement it once as a **concrete** method on the
|
||||
device-type base class (here, in this repo).
|
||||
- If no — it talks to the device itself (a specific REST endpoint, a CLI command,
|
||||
a vendor-specific payload format) — it belongs in the concrete driver as the
|
||||
implementation of an **abstract** method the base class declares.
|
||||
|
||||
Concretely: matching/comparison/reconciliation algorithms, orchestration flows, and
|
||||
generic data shapes belong here. Only the actual device communication belongs in
|
||||
`vendor/napalm-<name>`.
|
||||
|
||||
**Worked example — firewall rule diff/apply** (`FirewallDriver`):
|
||||
|
||||
```python
|
||||
class FirewallDriver(DeviceTypeDriver):
|
||||
# Abstract — every driver implements its own device communication.
|
||||
def get_firewall_rules(self) -> List[FirewallRuleDict]: raise NotImplementedError
|
||||
def apply_firewall_rule(self, rule: FirewallRuleDict, *, uuid: Optional[str] = None) -> Dict[str, Any]: raise NotImplementedError
|
||||
def commit_firewall_rules(self) -> Dict[str, Any]: raise NotImplementedError
|
||||
|
||||
# Concrete — the matching/comparison/orchestration algorithm is identical
|
||||
# for every firewall vendor, so it lives here once.
|
||||
def diff_firewall_rules(self, desired: List[FirewallRuleDict]) -> FirewallRuleDiffDict:
|
||||
... # matches self.get_firewall_rules() against `desired` by description
|
||||
|
||||
def apply_firewall_ruleset(self, desired: List[FirewallRuleDict]):
|
||||
... # computes the diff, calls apply_firewall_rule() per change, commits
|
||||
```
|
||||
|
||||
A new driver (FortiGate, pfSense, …) gets `diff_firewall_rules`/
|
||||
`apply_firewall_ruleset` for free the moment it implements the three abstract
|
||||
methods — it never needs to reimplement the reconciliation logic itself.
|
||||
|
||||
This mirrors a similar split already documented on the consumer side, in NetOrk's
|
||||
`docs/ARCHITECTURE.md` ("Device Warnings — Trennung von Erkennung und
|
||||
Präsentation"): drivers return raw signals, the higher layer gives them meaning.
|
||||
Same shape of separation, different axis — device-specific vs. generic here,
|
||||
detection vs. presentation there.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user