napalm-fortigate
A NAPALM driver for Fortinet FortiOS, built on the FortiOS REST API rather than SSH screen-scraping.
Status: beta. All twenty-six NAPALM getters are accounted for -- twenty implemented against real hardware, six refused with a documented reason -- and the configuration lifecycle is complete: merge, replace, commit, commit-confirm and rollback. Nothing here reports data it has not actually seen.
Why this exists
There are three prior attempts at a NAPALM FortiOS driver. All are abandoned and none installs on a current Python:
| project | last commit | problem |
|---|---|---|
napalm-automation-community/napalm-fortios |
Apr 2022 | last PyPI release 2018; depends on pyFG, which needs pip.req (removed in pip 10, 2018); its tests still import napalm_base, the pre-2.0 package name, so the suite has not run in years |
n3s0/napalm-fortios + n3s0/pyfg |
Apr 2025 | packaging fixed and it installs, but pyfg assigns self.host = host where the parameter is hostname, so constructing the client raises NameError immediately |
glennake/napalm-fortinet |
Aug 2022 | best foundation of the three (netmiko, no pyFG, correct imports) but abandoned mid-build; its test harness has never worked, since open() overwrites the test double with a real SSH connection |
The common thread is SSH. Every one of them screen-scrapes a CLI, which means a parser per command, a dependency on terminal pagination behaviour, and -- on any unit with FortiToken enforced -- an authentication problem NAPALM cannot solve, because it has nowhere to get a TOTP code from.
REST avoids all of that. An API token is not subject to 2FA, responses are JSON,
and there is no pager to disable. It also removes the need for the
config system console / set output standard write that netmiko performs on
every connect, which is a configuration change on a production firewall.
Why "fortigate" and not "fortios"
NAPALM names drivers after the operating system -- ios, junos, eos,
nxos, panos -- so fortios would be the conventional choice, and it is what
most people would guess.
It was not used, for two reasons. The name is claimed on PyPI by the abandoned
2018 package and used by three GitHub repositories, none of them maintained, so
it carries confusion without carrying any compatibility worth having: the old
driver has not been installable since 2018, so nobody has a working
get_network_driver("fortios") to stay compatible with. And fortigate is more
honest about scope, since this driver targets FortiGate units and will not work
against FortiSwitch or FortiAnalyzer, which have different APIs.
The name to use is therefore fortigate:
get_network_driver("fortigate")
Inside napalm_fortigate/_rest.py the client is still called FortiOSClient,
because the API it speaks to is genuinely named the FortiOS REST API, and
because that module is the piece most likely to be extracted one day.
Requirements
- Python 3.10+
napalm >= 5.0.0requests(already a NAPALM dependency, so this driver adds nothing new)
Developed against FortiOS 7.4.x. Commit-confirm (revert_in) needs FortiOS
7.2.1 or later and a super_admin token; see Commit confirm
for what has been measured on which firmware.
Install
pip install napalm-fortigate
To pin an exact release from the repository instead:
pip install "napalm-fortigate @ git+https://gitlab.com/ggiesen/napalm-fortigate.git@v0.2.2"
Setting up an API token
The driver authenticates with an API token, not a password. On the FortiGate,
create a read-only access profile and an API user bound to it. Note that the
built-in profiles are not read-only -- prof_admin grants read-write across
every group -- so the profile has to be created:
config system accprofile
edit "napalm-ro"
set secfabgrp read
set ftviewgrp read
set authgrp read
set sysgrp read
set netgrp read
set loggrp read
set fwgrp read
set vpngrp read
set utmgrp read
set wifi read
next
end
config system api-user
edit "napalm-ro"
set accprofile "napalm-ro"
set vdom "root"
config trusthost
edit 1
set ipv4-trusthost <your-source-address>/32
next
edit 2
set type ipv6-trusthost
set ipv6-trusthost <your-source-address>/128
next
end
next
end
execute api-user generate-key napalm-ro
generate-key prints the token once. A dual-stacked management network wants
both entries: what an IPv4-only trusthost table does with an IPv6 source has
not been measured here, so do not rely on it either way. A trusthost is not
optional in practice:
without one the token is usable from anywhere that can reach the admin port. Be
aware that pinning to a single dynamic address will break the driver the day it
changes.
For configuration management the profile needs write access, so create a second API user rather than loosening the read-only one.
Usage
from napalm import get_network_driver
driver = get_network_driver("fortigate")
device = driver(
hostname="192.0.2.1",
username="", # unused; the token identifies the caller
password="",
optional_args={
"api_token": "<token>",
"port": 8443, # check admin-sport, it is often moved
"ssl_verify": False, # FortiGates ship a self-signed certificate
},
)
device.open()
print(device.get_facts())
device.close()
With napalm-ansible, Salt or Nornir, pass the token as password rather than
in optional_args. Those tools treat NAPALM's password as a secret and keep
it out of logs, while optional_args is ordinary task data: a token in
optional_args["api_token"] lands in verbose task output and job records.
The driver accepts either; optional_args["api_token"] is for direct Python
use, where nothing logs the arguments.
ssl_verify defaults to True. Turning it off should be a deliberate choice;
pointing it at a CA bundle path is better where you can. It is also the only
thing that decides: the driver ignores HTTPS_PROXY, REQUESTS_CA_BUNDLE,
CURL_CA_BUNDLE and ~/.netrc on purpose, so the shell's ambient settings
cannot quietly override the choice, route the token through a proxy, or swap
it for a netrc password. hostname may be an IPv6 literal, with or without
brackets, zone id included.
Coverage
Verified against a FortiGate 60F on FortiOS 7.4.12.
| getter | source |
|---|---|
get_facts |
monitor/system/status, monitor/system/interface, cmdb/system/dns |
get_interfaces |
monitor/system/interface + cmdb/system/interface |
get_interfaces_ip |
monitor/system/interface + cmdb/system/interface |
get_interfaces_counters |
monitor/system/interface |
get_arp_table |
monitor/network/arp |
get_config |
monitor/system/config/backup -- see the privilege note below; sanitized=True is honoured (below) |
get_environment |
monitor/system/resource/usage, monitor/system/sensor-info |
get_optics |
monitor/system/interface/transceivers |
get_lldp_neighbors |
monitor/network/lldp/neighbors |
get_lldp_neighbors_detail |
monitor/network/lldp/neighbors |
get_bgp_neighbors |
monitor/router/bgp/neighbors + neighbors6 + paths + paths6 + cmdb/router/bgp |
get_route_to |
monitor/router/ipv4, monitor/router/ipv6 |
get_ntp_servers |
cmdb/system/ntp, falling back to monitor/system/ntp/status when NTP is fortiguard with no explicit servers |
get_ntp_stats |
monitor/system/ntp/status |
get_firewall_policies |
monitor/firewall/policy + cmdb/firewall/policy |
get_bgp_config |
cmdb/router/bgp |
get_bgp_neighbors_detail |
monitor/router/bgp/neighbors + neighbors6 + paths + paths6 + cmdb/router/bgp |
get_network_instances |
cmdb/system/vdom + cmdb/system/interface |
get_snmp_information |
cmdb/system.snmp/sysinfo + community |
get_vlans |
cmdb/system/interface |
All twenty-six getters are now either implemented or refused with a reason.
Two API quirks worth knowing if you extend this. SNMP is namespaced under
system.snmp, with a dot -- cmdb/system/snmp/sysinfo returns HTTP 400 and
cmdb/system.snmp/sysinfo works. And router/bgp/paths names the neighbour
each path was learned_from, which is the only way to get a per-peer prefix
count: the neighbors-statistics and paths-statistics endpoints return
device-wide totals only.
Where FortiOS cannot answer
Several fields are -1 or empty by necessity, not by omission. They are worth knowing before building anything on top:
| field | why |
|---|---|
get_facts -> uptime |
not reported anywhere in the REST tree |
get_environment -> memory |
given as a percentage, with no total published to convert against |
get_bgp_neighbors -> sent_prefixes, uptime |
not published at all; received/accepted counts are derived from router/bgp/paths |
get_bgp_neighbors_detail -> message counts, negotiated timers, flap count, neighbour router ID |
not published; NAPALM's model describes far more than FortiOS reports |
get_network_instances -> route_distinguisher |
FortiOS attaches no RD to a VDOM |
get_route_to -> age |
routes carry an install date, not an age, and no reference clock is exposed |
get_interfaces -> last_flapped |
FortiOS records no interface flap timestamp |
get_interfaces_counters -> discards, multicast, broadcast |
totals only; no per-cast breakdown |
get_lldp_neighbors_detail -> capabilities |
not advertised in the LLDP response |
get_ntp_stats -> referenceid, when, hostpoll |
no FortiOS equivalent |
get_snmp_information -> community mode |
always ro: FortiOS SNMP communities are read-only, there is no write community |
get_bgp_config -> group multihop_ttl |
always 0: FortiOS sets ebgp-multihop-ttl per neighbour, not per group |
Five things worth knowing, four of which only became apparent against real hardware:
uptime is always -1. FortiOS 7.4 does not report system uptime anywhere in
the REST monitor tree. Not system/status, not system/resource/usage, not
system/performance/status; the only uptime field in Fortinet's entire
Ansible module catalogue belongs to HA configuration. -1 is NAPALM's convention
for a fact the platform cannot supply, and is better than deriving a plausible
number from something else.
get_config(sanitized=True) strips credentials. Every set <field> ENC <value> line, PEM private keys, SNMP community strings and the cleartext
credential fields the driver knows about are replaced with <removed>, the way
NAPALM's core drivers do; FORTIOS_SANITIZE_FILTERS in
napalm_fortigate.fortigate is the table, for anyone who wants to run it over
text of their own. A cleartext secret under a field the table does not know
about survives, so treat the output as safer, not safe. format must be
"text" (anything else raises NotImplementedError) and full is accepted
and ignored: the backup endpoint has neither switch.
get_config needs more than a read-only token. monitor/system/config/backup
answers 403 to an access profile with read on every group, because a config
backup contains secrets. It needs write access to the system group. The rest of
the getters are happy read-only.
Some endpoints simply do not exist on smaller units.
monitor/system/sensor-info and monitor/system/interface/transceivers both
return 404 on a 60F, which is fanless and has no optical ports. The driver
treats those as "no sensors" and "no optics" rather than as errors, but code
that assumes a documented endpoint is present will break on the low end of the
range.
monitor/system/resource/usage is enormous by default. It returns every
historical sample and ran to 168KB on an idle unit. Always pass an interval.
Endpoint paths come from Fortinet's own fortinet.fortios Ansible collection,
which ships a machine-readable catalogue of the REST API. Parsing is written
against captured fixtures, not against those docs, so a getter's parsing is
only verified once its response has been seen. Two have not been: get_optics
and the sensor half of get_environment take their field names from the
catalogue, and the only response captured for either is the 404 below.
Deliberately not implemented
These raise NotImplementedError with an explanation, and that is not expected
to change.
| getter | why |
|---|---|
get_mac_address_table |
The only MAC-table endpoints are under switch-controller, which covers managed FortiSwitches. A FortiGate's own internal switch is not exposed. Would need diagnose netlink brctl over SSH. |
get_ipv6_neighbors_table |
No NDP endpoint exists. monitor/router/ipv6 returns routes, not neighbours. Would need diagnose ipv6 neighbor-cache list over SSH. |
get_ntp_peers |
A FortiGate is an NTP client. Reporting its configured servers as peers would be wrong. |
get_probes_config |
system/link-monitor is a failover health check, not an RPM/IP SLA probe. |
get_probes_results |
As above. |
ping, traceroute, cli |
The REST API has no endpoint that runs a command and returns its output. All 580 selectors in Fortinet's own module catalogue contain nothing of the sort, and monitor/network/ping, monitor/system/ping, the traceroute equivalents and monitor/system/exec all answer 404 on 7.4.12. monitor/system/config-script/upload does run CLI, but replies with a status envelope only -- a script of get system status returns success and no output -- so it is a configuration applicator, not a command runner. |
get_users |
cmdb/system/admin returns an empty list to an API-token caller no matter what the access profile grants -- verified with read-write on every group, and with the vdom and global scopes tried explicitly, while the same device lists two administrators over SSH. Returning {} would be reporting something known to be false, since every FortiGate has at least one administrator. |
Reaching the first two would mean a second credential -- an API token cannot open an SSH session -- plus an admin account exempt from FortiToken. That is a meaningful security cost for two getters, so the driver says no clearly instead of half-doing it.
VDOMs
Pass optional_args={"vdom": "..."} and every request carries that VDOM as a
query parameter. Verified on a FortiGate-VM running 7.4.12 with vdom-mode
set to multi-vdom:
- the parameter is attached to every
cmdbandmonitorrequest - all eighteen getters that unit supports return correctly when scoped to a named VDOM
- a VDOM that does not exist is rejected with HTTP 424 rather than silently ignored, so a typo fails loudly instead of quietly returning another VDOM's data
What has not been tested is separation between two VDOMs. The permanent
evaluation licence will not allow a second one -- creating it fails with
Command fail. Return code -4 (reached the maximum number of entries), despite
get system status advertising "Max number of virtual domains: 2", which
evidently counts internal VDOMs. Confirming that a getter scoped to VDOM A
cannot see VDOM B's data needs a licensed multi-VDOM device.
Commit-confirm works on single- and multi-VDOM units; see the commit-confirm notes on VDOMs for the one difference.
Configuration management
FortiOS has no candidate configuration. Neither does Cisco IOS, and NAPALM's IOS driver has supported the full lifecycle for years by staging the candidate and applying it at commit. This driver does the same, holding the candidate in the driver rather than on the device.
| NAPALM method | FortiOS |
|---|---|
load_merge_candidate |
held in the driver; nothing is sent yet |
compare_config |
returns the staged commands |
commit_config |
config-revision/save, then config-script/upload, then config/save |
discard_config |
drops the staged text |
load_replace_candidate |
held in the driver; applied by commit_config via config/restore |
rollback |
config/restore of the revision commit_config took |
commit_config(revert_in=N) |
a revision, then a one-time automation stitch that restores it in N seconds, then config-script/upload; not saved -- see Commit confirm |
confirm_commit |
deletes the stitch, which cancels it, then config/save |
has_pending_commit |
whether the stitch exists on the unit with its trigger still ahead; raises if it fired or missed its time, False if it is inert -- see Commit confirm |
config-script/upload executes the script as it uploads it. There is no
upload-now-run-later for local scripts; the separate config-script/run
endpoint is for scripts pulled from a remote script server. So the upload
happens in commit_config, not in load_merge_candidate.
commit_config does not lie. This was the question worth answering before
trusting any of it, and it was tested against hardware: a script that fails
partway comes back as HTTP 500 with status: error and a cli_error field
carrying the full CLI transcript, naming the command that broke. That transcript
is included in the MergeConfigException. For example:
MergeConfigException: Config script failed; the device may be partially
configured. POST .../system/config-script/upload returned HTTP 500:
fortigate config system global
fortigate (global) set hostname "..."
fortigate (global) end
fortigate bogus-command
Unknown action 0
Note what that transcript shows: the valid commands ran before the bad one
failed. A failed commit can leave the device partially configured. The
revision taken at the start of commit_config is how you get back, and that
revision is taken before anything is applied -- if it cannot be taken, the
commit aborts rather than proceeding with no way back.
compare_config returns the commands to be applied, not a diff. FortiOS has no
server-side comparison, and the candidate is a fragment of CLI rather than a
full configuration, so diffing it against the running config would be
misleading. This is the same position
librouteros PR #383 takes, for
the same reason.
load_replace_candidate and rollback both go through
monitor/system/config/restore, which reboots the unit. Two things about
them are worth knowing before use.
They need a super_admin token. An access profile with read-write on every
group is not enough -- config/restore answers 403 to one. This is the same
shape of surprise as config/backup refusing a read-only token, one level up.
The driver says so in the exception rather than passing the bare 403 along.
The reboot is measured, not assumed. On a FortiGate-VM running 7.4.12 the
endpoint returns immediately with {"restore_started": true, "config_restored": true}, the unit stops answering about eleven seconds later, and is back roughly
forty-three seconds after the call. Hardware will be slower. Validated by
setting a hostname, taking a revision, changing it, restoring, and confirming it
came back.
rollback() restores the revision commit_config recorded, and refuses if no
commit has run on that connection. Rolling back to whatever revision happens to
be newest would be guessing at intent; list them with
monitor/system/config-revision and restore one by hand if that is what you
want.
Commit confirm
commit_config(revert_in=N) schedules its own undo on the unit before applying
anything:
- It takes a configuration revision, found again by a nonce in its comment.
- It creates a one-time automation stitch,
napalm-commit-confirm, whose CLI script runsexecute restore config flash <revision>N seconds from now. - It applies the change through
config-script/uploadand does not save.
confirm_commit() deletes the stitch, which cancels it, and saves. If nobody
confirms in time, the stitch fires and the unit restores the revision, which
reboots it. The revision predates the stitch, so nothing is left behind.
The stitch is the pending marker. has_pending_commit(), confirm_commit() and
rollback() all read it from the unit, so they give the right answer from a
fresh connection, a different host, or minutes later. cfg-save is never
touched.
Requirements
| requirement | why | if not met |
|---|---|---|
| FortiOS 7.2.1 or later | trigger-frequency once arrived in 7.2.1 |
CommitConfirmException, nothing changed |
super_admin token |
the scheduled restore runs execute restore under that profile, and rollback needs it |
anything less is untested; FortiOS is expected to refuse, not measured |
| a standalone unit, not an HA cluster | the stitch is configuration and syncs to every member, but revision ids are per member; what a cluster does with it has not been measured | CommitConfirmException, nothing changed; rollback is refused too |
reboot-upon-config-restore enable (the default) |
with it disabled a restore does not take effect until the next reboot | CommitConfirmException, nothing changed |
monitor/system/automation-stitch/stats |
reports the trigger time, used to read the unit's clock and check the schedule | CommitConfirmException, nothing applied |
revert_in between 60 and 86400 |
triggers fired 2 to 28 s late on the VM and 8 s on the 40F, and the driver allows for 30 s; a day is an arbitrary sane limit | CommitConfirmException, nothing changed |
Time is the unit's, not the caller's. A one-time trigger is written in the
unit's local time. The driver reads the unit's UTC time from the HTTP Date
header, reads its UTC offset back through a harmless throwaway stitch
(napalm-clock-<nonce>), then checks the real schedule against
what the unit reports before applying anything. Skew between the automation
host and the unit does not matter. The offset is probed fifteen hours out, so a
DST change between now and then makes it an hour off at the target; the unit's
read-back of the real schedule is the exact offset, and the driver corrects the
schedule from it rather than refusing. It gets three attempts in all, shared
with the window re-measure below; if the read-back still disagrees after that,
the commit is refused and nothing is applied. The window is re-measured after
the unit has confirmed the schedule: if waiting on a slow unit ate into it, the
schedule is moved so the caller gets the window asked for. The window is still
measured on the unit's clock, so an NTP step during it
moves the restore; keep units on NTP.
What has been measured, and where
Measured with tools/probe_scheduled_restore.py on a FortiGate-VM and by hand
on a FortiGate 40F. 7.6.x has not been tested at all.
| behaviour | 40F, 7.2.10 | VM, 7.4.12 |
|---|---|---|
| a one-time trigger fires once, at its time, not at creation | yes (CLI) | yes (REST) |
| deleting the stitch before its time cancels it | yes (CLI) | yes (REST) |
| a trigger time already past is refused | yes | not tested |
| the stitch, trigger and action can be created over cmdb REST | not tested | yes |
| the stitch restores the revision, then is gone | not tested | yes, single and multi VDOM |
| someone saves during the window: the restore still runs and cleans up | not tested | yes |
| the stats endpoint exists and gives the offset away | not tested | yes |
| a revision captures running, not saved, configuration | not tested | yes |
Triggers fired between 2 and 28 seconds late across the VM runs, and 8 seconds late on the 40F; the driver's checks allow for 30. On the VM the unit stopped answering about ten seconds after the stitch fired and was back about a minute after that. Hardware will be slower.
execute restore config flash asks for no confirmation when run from a script:
the transcript goes straight to Get config from local disk OK. File check OK.
and reboots. Fortinet's own
auto-restore KB article
uses the same primitive, a stitch running a CLI restore, for the same purpose.
cfg-save modes
The mechanism does not depend on cfg-save, but what "saved" means does, and so
do the side effects.
cfg-save |
during the window | not confirmed | confirm_commit |
|---|---|---|---|
manual |
change and stitch applied, unsaved | restore, reboot | cancels, then saves everything staged on the unit |
automatic |
change and stitch saved as they are applied | restore, reboot | cancels; the save is a formality |
revert |
change and stitch unsaved, so the unit's own revert timer runs as well | restore, reboot -- or, if the unit's own cfg-revert-timeout is shorter than revert_in, its own revert gets there first, which also undoes the change (expected, not measured) |
cancels, then saves, which also ends the unit's own revert window |
All three are exercised by the integration suite on the VM. The scheduled
restore itself was probed from manual.
Things to know, most of them about manual:
- A restore is the whole revision. Anything changed on the unit after the commit took its revision is rolled back too -- by anyone, including a save made during the window.
- Revisions capture running configuration, not saved. On a
manualunit, changes that were staged but unsaved when the commit started are in the revision, so an unconfirmed restore makes them permanent. - Confirming saves. On a
manualunit that includes anything else staged, such as the paging change SSH backup tools make on every login. A plaincommit_confighas always done the same. - Someone saving during the window saves the stitch too. It still fires, restores, and is gone afterwards, because the revision predates it. Nothing fired again in the following five minutes.
- With
revision-backup-on-logoutenabled, FortiOS saves a revision of its own whenever an admin session that changed something logs out, backup tools included. That does not confuse the commit, which finds its own revision by its nonce rather than by being newest, and it neither confirms nor cancels anything: a revision is a snapshot, not a save. Changes made over REST with an API token produce no such revision -- measured with the setting enabled andadmintimeoutat a minute -- so the driver's own traffic does not consume revision slots. Two things follow for the ones other sessions do write. - A revision taken during the window holds the pending change, and the
schedule. Revisions capture running configuration, so one written while a
commit is pending contains the unconfirmed change and
napalm-commit-confirm. Restoring that revision by hand later brings the change back with it. The schedule comes back inert: restored with a trigger time already in the past, the stitch was present but never fired, and the unit reported no next trigger for it. - A rotated-away revision fails loudly. The VM kept 20 revisions, dropping
the oldest as new ones arrived; a 40F has less flash, so check yours. The
commit's revision is the newest when it is taken, so losing it inside the
window would take a burst of other revisions. If it does happen the restore
fails, the stitch stays in place, and
has_pending_commitraises rather than reporting the change as confirmed. - If the stitch has fired but is still present, the restore did not take effect.
has_pending_commitandconfirm_commitraise rather than answer, androllback()restores the revision. Likewise if its time passed by more than three minutes without it firing (a clock step, or the unit was down): the change is live and nothing is armed,has_pending_commitraises, and eitherconfirm_commit(keep it) orrollback(undo it) resolves it. A schedule brought back by a by-hand restore is inert (the unit reports no trigger time for it); it is not pending, and the nextcommit_config(revert_in=...)removes it. confirm_commitrefuses inside the last thirty seconds of the window, where the trigger could fire between its check and its cancel; wait for the reboot and verify, or roll back.- A request that times out is reported as what it is, unknown, not as a failure. A config script that times out may have run in full, in part or not at all; the driver discards the candidate so a retry cannot run it blind, and says which revision to roll back to. A cancel or save that times out is repeated, since both are idempotent, after the unit answers again.
- The schedule's names are fixed, so two drivers cannot both hold a commit-confirm on one unit: FortiOS refusing the duplicate is the lock, and the second commit fails with nothing applied. The driver only obeys a schedule whose script is exactly what it writes; one edited by hand, or a driver-remembered revision that disagrees with it, is refused rather than restored.
confirm_commitraises when nothing is pending, as NAPALM's EOS and IOS drivers do. After a restore nothing is pending either, and returning quietly would report a rolled-back change as confirmed. Checkhas_pending_commitfirst, and verify the change when it says False.- An unplanned reboot inside the window is unmeasured. On a
cfg-save automaticunit the change and the schedule are saved to flash as they are applied, so a power loss or a reboot by another admin carries both across the boot. Whether a saved one-time trigger fires after boot has not been measured. If the boot outlasts the window the trigger is past-dated and will not fire, leaving the unconfirmed change in place, and the unit can report that one of two ways. If it still reports the past due time, the schedule is missed:has_pending_commitraises rather than answering. If it reports no trigger time at all, which is what the by-hand restore measurement showed, the schedule is inert:has_pending_commitanswers False and the nextcommit_config(revert_in=...)orconfirm_commit()removes it, so the change stays live with nothing flagging it. Which of the two a rebooted unit lands in is unmeasured;rollback()restores the revision in either case. Onmanualandreverta reboot discards both. - A second
commit_configwhile one is pending raisesCommitError. revert_incannot be combined withload_replace_candidate:config/restoresaves and reboots, so there is nothing to undo.- Don't change the driver's own API key in a push with
revert_in. A restore brings the old key back. - HA is refused, not untested-and-allowed:
revert_inandrollbackboth raise on a unit whosesystem ha modeis notstandalone. Fortinet documents that a restore on the primary reboots every member; nothing here has been measured on a cluster.
VDOMs
execute restore only exists in the global context. On a multi-VDOM unit the
script is wrapped in config global ... end; on a single-VDOM unit it runs as
is. Both restored correctly on the VM. Without the wrapper, a multi-VDOM unit
ran the script and silently did nothing, which is how this was found. The
automation objects are global either way. Commit-confirm together with
optional_args["vdom"] has not been tested.
Why not cfg-save revert, auto-script, or transactions
cfg-save revert is FortiOS's own safety net, and the first implementation
used it: switch to revert mode without saving, apply, and let the unit reboot to
its saved configuration unless confirmed. tools/probe_cfg_revert.py measured it
on 7.4.12. It worked from manual and revert, and from automatic the timer
never armed at all. It was dropped because revert mode turns any unsaved change
into a reboot. SSH tools that disable paging do it with config system console
/ set output standard, which is a configuration change, so a backup run against
a unit in revert mode reboots it -- every night, if backups are nightly. And a
unit would be left in revert mode if someone else saved during the window, since
that saves the switch too, which the driver cannot prevent.
system auto-script cannot delay anything. With start auto it runs the
script the moment it is created, and repeat 1 means one run in total. Measured
on a 40F running 7.2.10. A scheduled restore would restore immediately.
Workspace mode and REST transactions stage changes until commit and discard them on timeout. Nothing is live during the window, so there is nothing for a post-check to verify. That is atomicity, not commit-confirm -- worth having separately, since a config script that fails partway leaves earlier commands applied.
Development
The project uses uv.
uv sync
uv run pytest
uv run ruff format --check .
uv run ruff check .
uv run mypy napalm_fortigate
The unit tests run entirely against captured fixtures under
test/unit/mocked_data/; none touches a device. test/integration does: it is
outside pytest's default testpaths, skips itself without FGT_TEST_HOST and
FGT_TEST_TOKEN, and its destructive cases need --run-destructive.
See CONTRIBUTING.md for the conventions this project follows, particularly around capturing and sanitising fixtures.
Layout
napalm_fortigate/
_rest.py FortiOS REST client. Imports nothing from napalm.
fortigate.py The driver. All NAPALM coupling lives here.
tools/
boot-fortigate.sh boots a disposable FortiGate for the integration job
probe_scheduled_restore.py measures the scheduled restore commit-confirm uses
probe_revisions.py measures how stored revisions behave around it
probe_cfg_revert.py measures cfg-save revert, the design it replaced
_probe.py the probes' interlock, restore and scrubbing log output
_rest.py is kept free of NAPALM imports on purpose. If a standalone FortiOS
REST library ever turns out to be worth having, it can be lifted out without
modification. Until something other than this driver wants it, splitting the
repository would only buy release coordination overhead -- which is roughly how
pyFG came to strand the original driver.
Branching
masteris the production branch and is protected.- Work happens on feature branches, merged via merge request.
- The pipeline runs formatting, linting, type checking and unit tests on every push; a merge request shows its branch's pipeline. All four must pass before merge.
- Releases are cut from
masteras avMAJOR.MINOR.PATCHtag. Thepublishjob builds and uploads to PyPI with Trusted Publishing (OIDC), so no API token is stored anywhere, and it iswhen: manual: tagging never publishes on its own.
Changelog
See CHANGELOG.md.
Licence
Mozilla Public License 2.0. See LICENSE.
Metadata
Release files for napalm-fortigate 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| napalm_fortigate-0.2.2.tar.gz | 74.6 kB | Without provenance | Details for napalm_fortigate-0.2.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| napalm_fortigate-0.2.2-py3-none-any.whl | Python 3 | none | any | Details for napalm_fortigate-0.2.2-py3-none-any.whl |
Total release size: 125.7 kB
Release files / napalm_fortigate-0.2.2.tar.gz
| Download URL | napalm_fortigate-0.2.2.tar.gz |
|---|---|
| Size | 74.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
74665184d95d70729a10e424c0fc3d55ca98de3df51d649746f8c7d29baabebc
|
|
BLAKE2b-256 checksum How to use checksums |
ccd976e42659c77f9dfc22f39c07b63c63984157ff7769db94a325d6182faca0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / napalm_fortigate-0.2.2-py3-none-any.whl
| Download URL | napalm_fortigate-0.2.2-py3-none-any.whl |
|---|---|
| Size | 51.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
31729d841240ff9a9032ffc97d6875b1f8930b598b12d9035d0e9d5dcecb5e87
|
|
BLAKE2b-256 checksum How to use checksums |
3e97ca01a03c615444dae9e1881fc737984050265794b10e2b424dd1ad981b86
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Security for napalm-fortigate 0.2.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.