util

package
v1.0.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultHubSocket = "/run/factum2-worker/api.sock"

DefaultHubSocket is the unix HTTP path factum2-worker listens on and co-located CLIs probe. One constant so the two sides cannot drift.

View Source
const HubRPCTimeout = 60 * time.Second

HubRPCTimeout is the per-request bound for hub RPC (ServeHTTP after the websocket dies, CLI HTTP clients). Canonical here so util does not import worker.

Variables

This section is empty.

Functions

func ConnectDatabase

func ConnectDatabase(config *ConfigDB) (*gorm.DB, error)

ConnectDatabase opens Postgres and does not apply schema migrations. Call this from the web server and CLIs. Schema changes belong in the dedicated `migrate` command (cmdbase.Migrate → MigrateDatabase); running migrations as a side effect of start/sync/createadmin rewrites tables while factum2-web may already be serving.

func FactumHTTP added in v1.0.2

func FactumHTTP(cfg *ConfigFactum) (client *http.Client, baseURL string, viaSocket bool, err error)

FactumHTTP picks a client for Factum API calls. The local worker unix socket is used when it is present and dialable; otherwise HTTPS to cfg.URL. After a successful unix Dial, RPC 502s/timeouts are not retried over HTTPS - worker-net :443 may already be closed. Unix requests send no Authorization header (the socket ACL is the auth); HTTPS keeps the bearer.

func FetchRemoteConfig

func FetchRemoteConfig[T any](factumConfig *ConfigFactum, path string) (*T, error)

FetchRemoteConfig GETs path from the Factum API and unmarshals the JSON into a new T. Transport is selected by FactumHTTP (unix socket when the probe succeeds, otherwise HTTPS to factumConfig.URL with "Authorization: Bearer "+factumConfig.Token - see web.Controller.checkServiceToken). A unix 502 is returned as an error; it is not retried over HTTPS. Shared by CLI tools that typically run on a different host than the primary (factum2-icinga, factum2-oxidized, factum2-dns, factum2-librenms-cli, factum2-prometheus) to fetch their database-backed settings instead of needing a local copy - each package wraps this in its own FetchRemoteConfig returning its own config type, see e.g. internal/librenms/remote_config.go.

func FormatName

func FormatName(defaultDomain string, name string) string

FormatName appends defaultDomain when name has no '.' (a short hostname). Names that are already FQDNs (or IPv4 literals) are returned unchanged. An empty defaultDomain is a no-op so callers don't get a trailing dot.

func GetOrCreateSettings

func GetOrCreateSettings(db *gorm.DB) (*models.Settings, error)

GetOrCreateSettings returns the single Settings row (id=1), creating it with zero-valued fields on first use so callers always have a row to read - most settings (integration URLs/tokens like Netbox's, previously only configurable via the YAML config file) are now database-backed and editable from the admin UI instead, so both the web API and any CLI/sync code that needs them goes through here.

func HubSocketPath added in v1.0.2

func HubSocketPath(yamlOverride string) string

func MigrateDatabase

func MigrateDatabase(db *gorm.DB) error

MigrateDatabase applies schema migrations then cfgmgmt.Seed. Only cmdbase.Migrate (and tests) should call this — not ConnectDatabase.

Postgres: goose SQL in internal/dbmigrate (existing AutoMigrate DBs are stamped at the baseline version). SQLite: GORM AutoMigrateAll, for unit tests only.

func OrganizationEnabled added in v1.0.3

func OrganizationEnabled(db *gorm.DB) bool

OrganizationEnabled reports Settings.OrganizationEnabled (nil/false = off). Disabling the flag only hides the Customers/Contacts menu — it does not touch customer or contact rows.

func OxidizedEnabled added in v1.0.4

func OxidizedEnabled(db *gorm.DB) bool

OxidizedEnabled reports Settings.OxidizedEnabled (nil/false = off). Disabling the flag only hides the Oxidized menu and GUI browser — it does not touch device backup flags or oxidized's own router.db.

func Pprint

func Pprint(data any)

print structures JSON formatted

func ShortName

func ShortName(name, defaultDomain string) string

ShortName strips the trailing ".defaultDomain" suffix from a fully qualified name - the inverse of FormatName.

func StructToString

func StructToString(data any) string

Return data as JSON formatted string

Types

type CommonConfig

type CommonConfig struct {
	DefaultDomain string `json:"default_domain"`

	// SmtpHost/Port/User/Pass/TLSMode and EmailSender are the shared
	// outbound mail relay settings (Settings.Smtp*/EmailSender, edited on
	// the admin UI's Factum > Email tab) - factum2-icinga-notifications
	// is the first consumer, but any tool needing to send mail can reuse
	// these instead of having its own copy.
	SmtpHost    string `json:"smtp_host"`
	SmtpPort    uint16 `json:"smtp_port"`
	SmtpUser    string `json:"smtp_user"`
	SmtpPass    string `json:"smtp_pass"`
	SmtpTLSMode string `json:"smtp_tls_mode"`
	EmailSender string `json:"email_sender"`
}

CommonConfig holds settings shared by every remote-config-fetching CLI tool, not just one of them: DefaultDomain (Settings.DefaultDomain, edited in the admin UI's Factum tab) is needed by DNS (the $ORIGIN of the generated zone file) as well as Icinga/LibreNMS/Oxidized/Prometheus sync (matching factum device names against fully-qualified DNS names).

Every service's REST response and runtime Config type embeds this instead of each declaring its own copy of the fields. Tools that don't need any other service-specific config (currently just factum2-worker agents) can fetch this alone from web.ApiCommonConfig (GET /api/common-config).

func NewCommonConfig

func NewCommonConfig(settings *models.Settings) CommonConfig

NewCommonConfig builds a CommonConfig from the Settings row. Every web.ApiXxxConfig handler uses this instead of repeating the same lines.

type ConfigAgentRoot

type ConfigAgentRoot struct {
	Factum ConfigFactum `yaml:"factum"`
	Worker ConfigWorker `yaml:"worker"`
}

Agents get most of their configuration from the Factum API - factum2-worker is the one exception, needing its own local Worker section (worker.listen/ .token/.roles/.commands - see the comment on ConfigWorker for why these must stay local rather than fetched remotely).

type ConfigDB

type ConfigDB struct {
	Host     string `boa:"configonly" yaml:"host"`
	Port     string `boa:"configonly" yaml:"port"`
	User     string `boa:"configonly" yaml:"user"`
	Pass     string `boa:"configonly" yaml:"pass"`
	Database string `boa:"configonly" yaml:"database"`
}

type ConfigDNS

type ConfigDNS struct {
	CommonConfig
	DestFile        string
	IgnoreModels    string
	IgnorePlatforms string
}

ConfigDNS is a runtime-only DTO, not part of ConfigRoot - same pattern as ConfigIcinga: every field has a DB-backed equivalent (Settings.DnsDestFile/DnsIgnoreModels/DnsIgnorePlatforms, plus Settings.DefaultDomain via the embedded CommonConfig), fetched over REST by internal/dns.FetchRemoteConfig/RemoteClient from web.ApiDNSConfig.

type ConfigDeviceSync

type ConfigDeviceSync struct {
	CommonConfig
	VRFInGlobal  []string
	DeviceStates []string
	DeviceIgnore []string
	// VlanGroupName is the Netbox VLAN Group every synced VLAN is created
	// in (Settings.DeviceSyncVlanGroupName) - "" disables VLAN sync.
	VlanGroupName string
	// InventoryMaps is sync_source → netbox_type from cfgmgmt service
	// types (ELINE→evpl, ELAN→vpls, L3VPN→vrf). Empty means device-sync
	// falls back to ELINE→evpl only.
	InventoryMaps map[string]string
	Auth          map[string]ConfigDeviceSyncAuth
}

ConfigDeviceSync is a runtime-only DTO, not part of ConfigRoot - same pattern as ConfigNetbox/ConfigOxidized: every scalar-ish field has a DB-backed equivalent (Settings.DeviceSyncVRFInGlobal/DeviceStates/ DeviceIgnore, all newline-separated text; Auth comes from the models.DeviceSyncAuth table instead of a Settings column, since it's a list of credentials, not a single value), fetched over REST by internal/device-sync.FetchRemoteConfig from web.ApiDeviceSyncConfig. factum2-device-sync-cli runs on the primary and talks to factum2-web over REST (util.WithoutHubSocket), not the hub unix socket. The Netbox client itself isn't fetched here - internal/netbox.RemoteClient/FetchRemoteConfig already does that (GET /api/netbox-config), and device-sync reuses it rather than duplicating Netbox credentials in a second config type.

type ConfigDeviceSyncAuth

type ConfigDeviceSyncAuth struct {
	Username string
	Password string
}

ConfigDeviceSyncAuth is the username/password internal/device-sync uses to log into a device - keyed by device name in ConfigDeviceSync.Auth, with a "default" entry as the fallback for devices without their own entry.

type ConfigFactum

type ConfigFactum struct {
	// URL/Token are optional at the boa level - like ConfigWorker.Roles,
	// not every subcommand of every binary that embeds ConfigFactum
	// actually calls out to the primary (e.g. "factum2-worker start" never
	// does, only "run" does, via internal/worker.RunRemote), so requiring
	// them unconditionally would force values into a config file that
	// aren't needed for that particular subcommand. Call sites that do
	// need them (RunRemote, internal/factum.FactumClient) check for an
	// empty URL/Token themselves and fail with a clear error instead.
	URL string `boa:"configonly" yaml:"url" optional:"true"`
	// Token authenticates server-to-server callers (internal/factum's HTTP
	// client, used by e.g. factum2-dns and factum2-librenms-cli, the latter
	// typically running on a different host than the primary) against the
	// primary's Settings.FactumApiToken, sent as "Authorization: Bearer
	// <token>". See web.Controller.RequireAPIAuth.
	Token string `boa:"configonly" yaml:"token" optional:"true"`
	// Socket is the local unix API path when this CLI is co-located with
	// factum2-worker. Empty uses DefaultHubSocket unless FACTUM_WORKER_API_SOCKET
	// overrides it; "none"/"0" disables the socket (CLI-only escape hatch).
	// Primary-side CLIs should call WithoutHubSocket rather than relying on
	// operators to set this.
	Socket string `boa:"configonly" yaml:"socket" optional:"true"`
}

func WithoutHubSocket added in v1.0.6

func WithoutHubSocket(cfg ConfigFactum) ConfigFactum

HubSocketPath resolves the unix socket. yamlOverride is ConfigFactum.Socket (CLI) or ConfigWorker.APISocket (listener). FACTUM_WORKER_API_SOCKET is the single relocation / disable knob both sides honor when the yaml override is empty. Values "none" and "0" (yaml or env) mean "no socket" (CLI: force HTTPS; worker: Start error). WithoutHubSocket returns a copy of cfg that never probes the worker unix socket. Primary-side CLIs (device-sync and anything else that runs next to factum2-web) must use this: a co-located factum2-worker still listens on the socket, and FactumHTTP would otherwise send every call over hub RPC. Dest-host CLIs (dns, icinga, …) keep the default probe.

type ConfigIcinga

type ConfigIcinga struct {
	CommonConfig
	URL      string
	Username string
	Password string

	HostsFile string
	UsersFile string

	// IgnoreDevices is a newline-separated list of device names to skip
	// entirely (Settings.IcingaIgnoreDevices).
	IgnoreDevices string
	// DefaultNotification is Go text/template source executed with
	// .Device for a host that has no CfAlarmDestination
	// (Settings.IcingaDefaultNotification). Literal Icinga lines with no
	// {{ }} still render unchanged. The result is inserted into the host
	// object via hostTemplateData.Options.
	DefaultNotification string

	// HostTemplate/DependencyTemplate/UserTemplate are Go text/template
	// source, executed by internal/icinga.FactumIcingaClient - see that
	// package for the data each is executed with.
	HostTemplate       string
	DependencyTemplate string
	UserTemplate       string
}

ConfigIcinga is a runtime-only DTO, not part of ConfigRoot - unlike ConfigLibrenms's Sync maps, every field here already has a DB-backed equivalent (Settings.IcingaApiURL/User/Pass/HostsFile/UsersFile/ IgnoreDevices/DefaultNotification/HostTemplate/DependencyTemplate/ UserTemplate), so factum2-icinga - which typically runs on a different host than the primary - fetches this entirely over REST (internal/icinga.FetchRemoteConfig/RemoteClient, GET /api/icinga-config, served by web.ApiIcingaConfig from the Settings row) rather than reading any of it from local YAML.

type ConfigLdapWriteback

type ConfigLdapWriteback struct {
	BindDN   string `boa:"configonly" yaml:"bind_dn" optional:"true"`
	Password string `boa:"configonly" yaml:"bind_password" optional:"true"`
}

ConfigLdapWriteback is the elevated LDAP/AD identity permitted to change another user's password (web.ApiForgotPassword/ApiResetPassword, web.ApiMeUpdate) - deliberately separate from Settings.LdapBindDN/ LdapBindPassword (the read-only search/bind service account, DB-stored and returned in full by GET /api/admin/settings). Password write-back needs much stronger directory permissions than a search bind, so unlike every other LDAP setting it stays config-file-only and is never exposed through the Settings API - same reasoning as ConfigWeb.JWTSecret. Both fields are optional since most installs never enable LDAP password write-back at all (Settings.LdapAllowPasswordChange defaults to off).

type ConfigLibrenms

type ConfigLibrenms struct {
	CommonConfig
	URL string
	Key string

	// PersistentDevices is a newline-separated list of LibreNMS hostnames
	// or display names sync never quarantines or deletes - see
	// Settings.LibrenmsPersistentDevices's doc comment.
	PersistentDevices string
	// DelayedDeleteEnabled/DelayedDeleteDays control the LibreNMS delete
	// path - see Settings.LibrenmsDelayedDeleteEnabled's doc comment.
	DelayedDeleteEnabled bool
	DelayedDeleteDays    int
	// RolesEnabled/InterfacesDisabled are newline-separated lists of
	// regexes - see Settings.LibrenmsRolesEnabled's doc comment.
	RolesEnabled       string
	InterfacesDisabled string

	// SNMPVersion/SNMPCommunities are used when creating devices in
	// LibreNMS - see Settings.LibrenmsSNMPVersion/LibrenmsSNMPCommunities's
	// doc comments. SNMPCommunities is newline-separated, same convention
	// as RolesEnabled/InterfacesDisabled.
	SNMPVersion     string
	SNMPCommunities string
}

ConfigLibrenms is a runtime-only DTO, not part of ConfigRoot - same pattern as ConfigIcinga/ConfigDNS/ConfigOxidized: every field has a DB-backed equivalent (Settings.LibrenmsApiURL/LibrenmsApiToken/ LibrenmsPersistentDevices/LibrenmsDelayedDeleteEnabled/ LibrenmsDelayedDeleteDays/LibrenmsRolesEnabled/LibrenmsInterfacesDisabled), fetched over REST by internal/librenms.FetchRemoteConfig/RemoteClient from web.ApiLibrenmsConfig - so factum2-librenms-cli, which typically runs on a different host than the primary, doesn't need any local librenms config of its own. LibreNMS's own MySQL credentials aren't part of this struct at all - NewFactumLibrenmsClient reads those directly from LibreNMS's .env file on disk instead, since factum2-librenms-cli assumes co-location with the LibreNMS server.

type ConfigNetbox

type ConfigNetbox struct {
	CommonConfig
	URL   string
	Token string
}

ConfigNetbox is a runtime-only DTO, not part of ConfigRoot - same pattern as ConfigIcinga: every field has a DB-backed equivalent (Settings.NetboxApiURL/NetboxApiToken), fetched over REST by internal/netbox.FetchRemoteConfig/RemoteClient from web.ApiNetboxConfig - so factum2-librenms-cli (which typically runs on a different host than the primary, without direct Postgres access) doesn't need its own copy of these credentials, or a direct DB connection, to build a Netbox client.

type ConfigOxidized

type ConfigOxidized struct {
	CommonConfig
	URL  string
	User string
	Pass string

	// DestFile is oxidized's own router.db - the file
	// internal/oxidized.FactumOxidizedClient.Sync writes the filtered
	// device list to (name:ip:model per line).
	DestFile string

	// IgnoreDevices/IgnoreManufacturers/IgnoreModels/IgnorePlatforms are
	// newline-separated lists (one value per line) - a device matching any
	// one of them is skipped during sync.
	IgnoreDevices       string
	IgnoreManufacturers string
	IgnoreModels        string
	IgnorePlatforms     string
}

ConfigOxidized is a runtime-only DTO, not part of ConfigRoot - same pattern as ConfigIcinga: every field has a DB-backed equivalent (Settings.OxidizedApiURL/User/Pass/DestFile/IgnoreDevices/ IgnoreManufacturers/IgnoreModels/IgnorePlatforms), fetched over REST by internal/oxidized.FetchRemoteConfig/RemoteClient from web.ApiOxidizedConfig.

type ConfigPrometheus added in v1.0.3

type ConfigPrometheus struct {
	CommonConfig
	DestFile  string
	ReloadURL string
	Module    string
	Auth      string

	IgnoreDevices       string
	IgnoreManufacturers string
	IgnoreModels        string
	IgnorePlatforms     string
}

ConfigPrometheus is a runtime-only DTO, not part of ConfigRoot - same pattern as ConfigOxidized: every field has a DB-backed equivalent (Settings.PrometheusDestFile/ReloadURL/Module/Auth/Ignore*), fetched over REST by internal/prometheus.FetchRemoteConfig from web.ApiPrometheusConfig. factum2-prometheus typically runs on the Prometheus/snmp_exporter host, not the primary, so it has no local prometheus YAML of its own.

type ConfigRoot

type ConfigRoot struct {
	DB            ConfigDB            `yaml:"db"`
	Factum        ConfigFactum        `yaml:"factum"`
	Web           ConfigWeb           `yaml:"web"`
	Worker        ConfigWorker        `yaml:"worker"`
	LdapWriteback ConfigLdapWriteback `yaml:"ldap_writeback"`
}

Primary configuation, enough to get it up and running. most coinfig is in database

var Config *ConfigRoot

type ConfigWeb

type ConfigWeb struct {
	Bind      string
	JWTSecret string
}

type ConfigWorker

type ConfigWorker struct {
	// Roles lists what this worker instance does. "primary" runs the
	// primary loop (dispatches ad hoc commands via "factum2-worker run" and
	// logs everything agents report); any other entry is the name of a
	// Commands entry that this instance additionally runs as an agent for
	// - so one worker process can be primary AND handle one or more
	// specific agent roles (e.g. ["primary", "librenms"]) at once, instead
	// of being purely one or the other. Every non-"primary" role must have
	// a matching Commands entry.
	//
	// optional:"true" because boa's required-field check would otherwise
	// treat this flat []string as required (unlike map fields, which
	// default to optional) for every cmd/worker subcommand, including ones
	// like "run" and "show-config" that never look at it - Worker.Start
	// does its own "at least one role" check instead, only when it
	// actually matters.
	Roles []string `boa:"configonly" yaml:"roles" optional:"true"`
	// Commands lists the predefined commands this worker is able to run as
	// an agent, keyed by name - Roles selects which of these (if any) this
	// particular instance actually activates. An agent only ever receives
	// commands it has both defined here and activated via Roles - there is
	// no separate addressing by node name.
	Commands map[string]ConfigWorkerCommand `boa:"configonly" yaml:"commands"`

	// Listen is the bind address (e.g. ":8443") for this agent's hub
	// listener (internal/worker.runHubListener), which the primary's
	// RemoteManager dials into - the reverse of this agent dialing out.
	// Empty (the default) disables the listener entirely.
	Listen string `boa:"configonly" yaml:"listen" optional:"true"`
	// Token is the shared secret this agent expects from the primary as
	// "Authorization: Bearer <Token>" on the hub connection - the primary
	// side of the same value lives on the matching models.WorkerNode row,
	// not in any config file.
	Token string `boa:"configonly" yaml:"token" optional:"true"`
	// TLSCert/TLSKey are the PEM files the hub listener serves WSS with.
	// Required for factum2-worker start (hub RPC carries config secrets;
	// there is no ws:// fallback). optional at boa level so show-config
	// and run do not force them.
	TLSCert string `boa:"configonly" yaml:"tls_cert" optional:"true"`
	TLSKey  string `boa:"configonly" yaml:"tls_key" optional:"true"`
	// APISocket is the unix HTTP listener for hub RPC. Empty uses
	// DefaultHubSocket / FACTUM_WORKER_API_SOCKET; "none"/"0" is invalid
	// for factum2-worker start (fail closed). Not a route on worker.listen.
	APISocket string `boa:"configonly" yaml:"api_socket" optional:"true"`
}

type ConfigWorkerCommand

type ConfigWorkerCommand struct {
	Cmd  string   `boa:"configonly" yaml:"cmd"`
	Args []string `boa:"configonly" yaml:"args"`
}

ConfigWorkerCommand is one predefined command a worker agent is allowed to run. Agents only ever execute commands looked up by name from this map - the command arriving over the hub connection is never used to build a shell command directly, so a compromised/forged message can at most pick one of these predefined commands.

type Response

type Response struct {
	Status  string `json:"status"`
	Message string `json:"message"`
	Data    any    `json:"data"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL