Valkyrian Labs logo

Command line

Every forti-pihole command, option and exit status, the environment and the files it uses.

Command line

1forti-pihole <command> [options]

forti-pihole is one executable with subcommands. Options may appear anywhere on the line. Commands that inspect never write anything; commands that change something say so below.

Options

OptionMeaning
-p, --profile NAMEThe forti profile (default: the configuration's profile, else forti's default profile)
--config PATHThe configuration file (default $FORTI_HOME/pihole/config.json)
--jsonMachine-readable output
-q, --quietsync prints nothing when everything applied; problems still go to standard error
-v, --verboseOffline devices and the reasons for every decision (devices, plan, sync, discover)
--versionforti-pihole <version> (engine forti-cli <revision>, forti-api <version>)
-h, --helpThe command summary

Command-specific options are listed with each command. A value-taking option without its value, a number outside its range, or an unknown option exits 2.

Getting started

setup

1forti-pihole setup [--pihole-url URL] [--password-stdin]

Connects to the FortiGate and Pi-hole, discovers networks and groups, and writes a starting configuration. When config.json does not exist it is created first (as init does). Then:

  • the FortiGate's model, hostname, FortiOS version, and the profile and its mode are shown;
  • --pihole-url URL sets pihole.url;
  • with a Pi-hole URL, setup logs in with the stored app password. When there is none (or it is bound to another host), it asks for one (input hidden at a terminal; --password-stdin reads one line from standard input), verifies it, and stores it as pihole/<name>/password in forti's secret store;
  • every LAN-side interface with an address is listed with a suggested strategy; interfaces with a DHCP server that the configuration does not list are added in observe mode;
  • device groups without a pihole_group get an unambiguous case-insensitive match of the Pi-hole group names, when there is one.

Writes only config.json and the secret store entry; nothing on the FortiGate or Pi-hole. An empty password exits 2; a Pi-hole that cannot be reached, refuses the password or runs an FTL older than 6.7.0 exits 4.

init

1forti-pihole init

Writes the starting configuration (packaging/config.example.json, also installed to share/forti-pihole/) to the configuration path, 0600. Refuses (exit 3) when the file exists.

discover

1forti-pihole discover [--json]

Networks, groups and Pi-hole at a glance: the networks table followed by the full plan. Read-only. --json prints {"networks": [...], "sync": {...}}.

Inspecting (read-only)

devices

1forti-pihole devices [-v] [--json]

The inventory, classified: groups with device counts, every online device with its group, source (protected, assignment, pin, rule, default, ambiguous) and fingerprint, overlaps, ambiguous devices, reassociation candidates (with their confirm commands) and warnings. -v adds offline devices and the reasons for each decision. report is an alias.

groups

1forti-pihole groups [--json]

Each device group, its address group (fp-grp-<group>), its member count, its Pi-hole group (and its id in --json), and whether it is privileged. A mapped Pi-hole group that does not exist is marked [missing in Pi-hole].

networks

1forti-pihole networks [--json]

Every non-WAN interface with an address: subnet, VLAN id (or interface type), configured mode (unconfigured when no network lists it), DHCP pools and device count.

slices

1forti-pihole slices [--propose] [--json]

For each slices and hybrid network: its slices (configured, or proposed by the allocation planner), utilization, conflicts, and groups short of addresses with the options for them. --propose includes every network, so you can see what the planner would carve out of any of them. Nothing is written. See Address slicing.

plan

1forti-pihole plan [-v] [--json]

Everything sync would do: the device report, pending FortiGate changes (creates, moves, retirements, devices blocked by the connectivity check, devices deferred by max_changes), Pi-hole client entry changes and notes, address-slice actions, and migrations (refused ones with their reasons and what would make them possible). Writes nothing, not even state.json.

--json has the keys taken_at, groups, devices, overlaps, candidates, warnings, pending (changes, operations, blocked, deferred, notes), status, pihole (desired, changes, findings, error), slicing (actions, findings), migrations, stages and operations.

references

1forti-pihole references IP [--json]

Everything on the FortiGate that refers to an IPv4 address: host objects, ranges and subnets that contain it, VIPs that map it, reservations and leases, each with the policies that reach it (directly or through nested groups, as source or destination, and whether they are enabled). Prints nothing refers to IP when nothing does.

status

1forti-pihole status [--json]

The configuration path and whether it is enabled or paused, the profile and its mode, the Pi-hole URL, the last sync, a plan waiting for approval, counts of known devices, pins, adoptions and reassociation candidates, and open operations. Exits 0 even when the FortiGate cannot be reached (device_error in --json).

history

1forti-pihole history [--count N] [--json]

The last N (default 20, 1-10000) write stages from history.jsonl: time, stage (objects, pihole, reservations, migrations), status, plan id and summary. --json includes rollback plans and operation ids.

Changing

sync

1forti-pihole sync [--prune] [--max-changes N] [--max-migrations N] [--no-migrations] [-q] [-v] [--json]

Plans, then applies, in stages: FortiGate objects (a plan waiting for approval from an earlier run goes first), Pi-hole client entries, reservations at addresses devices already hold, open migrations, new migrations. Needs "enabled": true (else it plans, writes nothing to the FortiGate or Pi-hole, and exits 7); while paused it plans, writes nothing to them and exits 0. It records what it observed in state.json either way. apply is an alias.

OptionMeaning
--pruneAlso delete managed Pi-hole entries that no configured network implies any more
--max-changes NAt most N devices change group, and at most N reservations are added, this run (1-1000; default the configuration's max_changes)
--max-migrations NAt most N new migrations start (0-50, default 1), on networks with manage_reservations
--no-migrationsNeither resume open migrations nor start new ones

Stages that wait for a person or fail are also printed to standard error, as is a Pi-hole connection error. A Pi-hole that cannot be reached skips the Pi-hole stage without failing the FortiGate stages.

pause, resume

1forti-pihole pause2forti-pihole resume

Sets or clears the pause flag in daemon-state.json. While paused, sync and forti-piholed write nothing and migrate refuses. Takes effect at once, also for a running forti-piholed.

assign

1forti-pihole assign MAC GROUP|auto

Pins a device to a group, a privileged one included; auto removes the pin so rules decide again. Changes only state.json; takes effect at the next sync. GROUP must be listed in the configuration.

confirm

1forti-pihole confirm ID [--reject]

Accepts a reassociation candidate (from devices): the new MAC is pinned to the old device's group, the old device's adoptions carry over, and the old entry is retired at the next sync. --reject records that they are different devices; the pair is never proposed again. See Randomized MACs.

adopt

1forti-pihole adopt MAC [--object NAME]... [--reservation]2forti-pihole adopt MAC --clear

Lets a migration of this device change administrator objects: each --object NAME (a host address object that follows the device to its new address), and with --reservation its own administrator DHCP reservation. Repeat adopt to add more; --clear forgets them all. Changes only state.json. See Migrations.

migrate

1forti-pihole migrate MAC [--dry-run] [--json]

Moves one device into its group's slice now, as a journaled operation, regardless of the network's manage_reservations. --dry-run prints the steps (with --json, the operation) and writes nothing. Running it needs "enabled": true and sync not paused (else exit 3).

ExitMeaning
0Done; a dry run; or the device only needs a reservation, or none (reserve / in-place is printed)
2No move is planned for that MAC (not on a slice network, no slice for its group, or already moving)
7Refused (the reason and what it needs are printed), or waiting for the device or an approval
8Failed

operations

1forti-pihole operations [--json]2forti-pihole operations show ID [--json]3forti-pihole operations resume ID4forti-pihole operations rollback ID

Lists every migration (newest first), shows one with its steps, errors and log, resumes it from the first step not done, or compensates its completed steps in reverse order. resume and rollback need "enabled": true. Exit: 0 done or rolled back, 7 waiting, 8 failed or rollback failed.

events

1forti-pihole events plan|install|remove --url http://HOST:PORT

The FortiGate automation stitch that notifies forti-piholed of config changes. plan prints the engine operations (the token as a placeholder); install generates the webhook token (stored as pihole/<profile>/webhook-token, bound to the FortiGate) when there is none, and creates fp-sync-event, fp-sync-webhook and fp-sync; remove deletes them. --url is required for every verb and must start with http:// or https://; without a port, 8735. Writes to the FortiGate whether or not the configuration is enabled. Exit: 0 done, 7 waiting for approval, 6 failed. See Runtime.

Exit status

StatusMeaning
0Success, including a paused sync
1Internal error, including a write refused by the guard transport (a bug: nothing was sent)
2Usage: unknown command or option, bad argument, a MAC that is not one, an unknown group
3Configuration or state: no or invalid config.json, no forti profile, a corrupt state.json, another run holding the lock, init over an existing file, not enabled or paused (migrate, operations)
4Connection or authentication: the FortiGate, or Pi-hole where a command needs it (setup, migrate, operations)
5The FortiGate has no such object or resource
6A device or API error; a failed sync stage
7A decision is needed: waiting for forti approve, a read-only profile, a stale plan, a sync while not enabled, a refused or waiting migration
8Partial or unknown outcome; a failed migration

Reading the FortiGate fails before anything is written: the message ends with nothing was changed.

Environment

VariableMeaning
FORTI_HOMEThe forti home (default ~/.forti); forti-pihole's files are in $FORTI_HOME/pihole
FORTI_SECRET_STOREfile or keychain: the engine's secret store (default: the Keychain on macOS, the file store elsewhere)

Files

PathWhat
$FORTI_HOME/pihole/config.jsonThe configuration (Configuration)
$FORTI_HOME/pihole/state.jsonDevices seen, pins, adoptions, candidates, a pending plan
$FORTI_HOME/pihole/history.jsonlOne line per write stage
$FORTI_HOME/pihole/operations/<id>.jsonMigration journals
$FORTI_HOME/pihole/lockThe run lock
$FORTI_HOME/pihole/daemon.json, daemon-state.json, events.jsonlforti-piholed (Runtime)