Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. Entries are generated from Conventional Commits via commitizen — do not hand-edit released sections, only the [Unreleased] section above them.
[Unreleased]¶
Fix¶
- asdk: logins to one management server are now paced together. Check Point rate-limits logins per management server machine — every domain whose active server is hosted on a Multi-Domain Server member shares that member's allowance — and
RateLimitercould not see that: it caps concurrency per target IP, and five domain logins to one MDS took five independent slots. A refusal (err_too_many_requests) now closes a per-member gate (LoginGate, one TTL row indistributed_locks) that every login attempt to that member — across tasks and gunicorn workers — waits at before trying, so one refusal per window is paid once rather than by every caller. Which member hosts a domain is read fromshow-domains(multi-domain-server) andshow-mdss, recorded on the domain's cache row, and rewritten whenever the domain's active server is re-resolved, so a domain that fails over to another member is paced against that member. Throttled attempts no longer count againstlogin_max_retriesand no longer fail after three windows (LOGIN_THROTTLE_MAX_WAITSis gone); a singlelogin()is bounded by the newArodonataSettings.login_max_wait(ARODONATA_LOGIN_MAX_WAIT, defaultDEFAULT_LOGIN_MAX_WAIT= 900 s, sized for 21 logins at 3/min) and, past it, raises the sameAuthenticationErrorcallers already catch, with aThrottlingErrorcause and a message that says how long it waited. Thelogin:{mgmt}:{domain}distributed lock (per-domain login contention) is now acquired with a timeout of whatever remains of the caller'slogin_max_waitbudget, instead of a hardcoded 90 s, so a second caller waiting for the same domain's login waits out the first caller's paced duration rather than failing at 90 s; a waiter must outlast the holder's paced attempt to avoid failing while the login it is waiting for is still legitimately in progress.login()is re-entrant — resolving a domain's active server logs into the system domain — and the whole chain shares onelogin_max_waitdeadline and renews every login lock it holds while waiting at the gate, sologin_max_waitis the total for onelogin()call including its nested logins, and a nested login can no longer let the outer domain lock lapse into another worker's hands. If a login lock is lost or stolen while its holder waits at the gate, the login is abandoned with anAuthenticationError("lost its login lock") rather than continuing under a lock it no longer owns. TheRateLimiterslot is now held for one login round trip instead of across the whole retry ladder, so a paced login no longer pins a domain-server slot for minutes.DatabaseLockManagergainedpeek_expiryandextend_lock;LoginGateis additive. A refused dedicated-session login (create_dedicated_session, used by the write paths) now closes the gate and does not consume a retry, as the shared login path already did; a dedicated-session login whose credentials are rejected now raisesInvalidCredentialsError(a subclass of theAuthenticationErrorit raised before, so existing handlers are unchanged). The temporary session-cleanup login is bounded bymin(remaining budget, login_throttle_window)rather than by the deadline of the login it was called to unblock. A rate-limiter slot timeout is deliberately not paced or retried: it remains aLockAcquisitionErrorraised afterrate_limit_slot_timeout(90 s), exactly as before this change, because a saturated slot is local contention for one target IP rather than a Check Point refusal, and waiting outlogin_max_waitfor it would hold the login lock for fifteen minutes without making a slot any more likely to free up. Finally, the design is reactive and three cases are knowingly uncoordinated: two hosts with separate databases sharing one API key each learn only from their own refusals; a SmartConsole user consuming the server's allowance is invisible to the gate; and a management server configured by hostname puts its system-domain logins (keyed on the configured host string) and its domain logins (keyed on the hosting member's IP) on two different gate keys — configure servers by IP to keep them on one. All three are safe degradations: the server remains the arbiter, and the cost is extra refusals, not failures. - db:
ensure_missing_columnscan now add aNOT NULLcolumn to a table that already has rows. It renderedALTER TABLE t ADD COLUMN c VARCHAR NOT NULLwith noDEFAULT, which SQLite rejects outright and PostgreSQL rejects on any non-empty table, so the exception propagated out ofDatabaseManager.initialize()and out ofArodonataClient.__aenter__— and because the schema hash is stored only after a successful scan, it did so on every start. ANOT NULLaddition now carries aDEFAULTderived from the column'sserver_default, else its own Python default, else a zero value for its declared type, rendered per dialect; a type with no portable default raises a message naming the column instead of emitting DDL the database will reject. The per-columnALTERand the per-indexCREATE INDEXare also now idempotent across concurrently starting workers: inspect-then-write is a TOCTOU window, so an "already exists" response is logged and skipped rather than re-raised, each statement running in its own savepoint so one tolerated failure cannot abort the rest of the migration. This applies to every table registered inSQLModel.metadata, including a consuming application's own models. The invariant for future additive columns: a new column must be nullable, orNOT NULLwith a renderable default. - cache: the
domainstable finally records what its columns claim.active_mdsandactive_serverheldmgmt_nameas a placeholder and thestandby_*columns were always empty; they now hold the hosting MDS member's name, the active domain server's name and the standby members/IPs/servers, and a newactive_mds_ipcolumn (added automatically on the consumer's nextinitialize()) holds the member's IPv4.arodonata.models.Domaingainsactive_mds_ipandstandby_mdss. A consumer that comparedactive_mdstomgmt_namewill see a different value on multi-domain servers; on a SmartCenter nothing changes.
v1.9.0 (2026-09-14)¶
Feat¶
- asdk: long-running Check Point tasks (
publish,revert-to-revision,install-policy,run-script) are now polled by the library instead of by cpapi.ApiTransport.api_callalways calls cpapi withwait_for_task=Falseand runs theshow-taskloop itself through the newTaskWaiter, so every poll is logged, OTel-spanned and covered by the rate-limiter slot the enclosing call already holds — previously a ten-minute revert was ~300 API calls this library could not see, rate-limit or trace. The poll interval backs off from 2 s to a 10 s ceiling instead of cpapi's flat 2 s (~70 polls for that revert instead of ~300), five consecutive poll failures are tolerated as cpapi did, and a wait that exceeds its budget raisesTaskTimeoutErrornaming the task-id, its last status, its progress and the elapsed time, where cpapi raised a bareTimeoutErrorwith no message at all. Backwards compatible by design:TaskTimeoutErrorsubclassesTimeoutErrorso existing handlers still catch it; afailedorpartially succeededtask still yieldssuccess=Falseon the returned response and still raises nothing; the returned response is still the finalshow-taskresult with the samedata,messageandcode; andwait_for_task=False, non-task commands andshow-taskitself are untouched paths. One deliberate difference from cpapi: a task status that is neithersucceedednor a recognized failure now yieldssuccess=Falserather than being reported as a success. New public namesTaskWaiter,TaskStatus,TaskTimeoutErrorandTaskPollErrorare additive;arodonata.configgainedDEFAULT_TASK_POLL_INITIAL_SECONDS,DEFAULT_TASK_POLL_MAX_SECONDSandDEFAULT_TASK_POLL_FAILURE_TOLERANCE. - cache:
RefreshOutcome.failed_domainsrecords the(mgmt, domain)pairs whose reload failed, so callers can distinguish a domain that was already fresh from one the refresh failed to repair — previously both were simply absent fromrefreshed_domains. Additive and defaults to empty; existing callers are unaffected.
Fix¶
- cache: an incremental (
smart-fast) refresh now refuses to apply a diff when the domain's history has not moved forward, and reloads the domain in full instead. Arevert-to-revisionmoves a domain's head backwards and discards the sessions in between, so the objects it restores were deleted in sessions that no longer exist and the ones it removes were added in sessions that no longer exist — no forward change list can describe that, and applying one leaves objects the revert removed sitting in the cache while ones it restored go missing. Previously the full reload after a revert happened only because Check Point refused theshow-changescall and every failure became a fallback: the right outcome by luck, dependent on server-side behaviour, and absent entirely from the bulkmode="incremental"path, which had no guard of its own.IncrementalRefreshernow compares the domain's current head against the cached baseline before fetching any diff, via a new read-onlyObjectService.fetch_last_published_session()(the existingrefresh_last_published_session()would have advanced the baseline and emptied the diff window). Only a head that is strictly earlier than the baseline counts as reverted: Check Point publish-times have minute resolution, so requiring the head to be strictly later made every prompt publish-then-read fall back to a full reload. The residual gap — reverting to a revision published inside the same minute as the baseline — still relies on theshow-changesfailure path, and closing it properly means diffing byfrom-sessionrather thanfrom-date. - asdk: a login that gets no answer no longer hammers the same address.
LoginCoordinatornow classifies a login failure three ways: a refusal (the server answered — throttling, or Check Point'sDatabase revision is in progressafter a revert) keeps its existing retry-with-backoff against the same address, because that is what clears a lockout; a timeout is retried for the full budget, because a slow server and a dead one look identical until one of them answers; and an unreachable address (refused or unroutable socket, or Check Point's ownUnable to connect to server...) stops at once. When a login does stop — on conclusive evidence, or after every attempt timed out — the cached SID is dropped, the domain's active server is re-resolved viashow-domains, and the login is retried there. If the domain has not moved, it fails with anAuthenticationErrornaming the address that went silent, rather than the emptyTimeoutErrorit used to be. This closes a gap in failover handling:FAILOVER_ERROR_CODESonly ever fired on a response code, so a domain server that simply stopped answering could never trigger the domain-IP refresh that the cachedactive_ipexists to support. NewServerUnreachableError(anApiConnectionError) is exported for callers who want the distinction; login failures still surface asAuthenticationError, so existing handlers are unchanged. The marker string is exported asarodonata.config.SERVER_UNREACHABLE_MESSAGE, and the wait a throttled login sits out isArodonataSettings.login_throttle_window(ARODONATA_LOGIN_THROTTLE_WINDOW, default 70 s) — the limit is server-side configuration rather than a universal constant, and a caller that knows it will not meet a real lockout should not be made to wait one out. - asdk: a login attempt now has its own per-attempt budget,
ArodonataSettings.login_timeout(ARODONATA_LOGIN_TIMEOUT, defaultDEFAULT_LOGIN_TIMEOUT= 120 s), instead of silently inheriting the transport's API default.LoginCoordinatornever passed a timeout at all, so there was no way to tune a login independently of ordinary API work — a deployment whose servers answer quickly can now lower it without touchingapi_timeout. The default deliberately matches the old inherited value: a domain-server login on a loaded MDS legitimately takes longer than a minute, and a tighter budget fails servers that are merely slow. The transport'sLOGIN ... TIMEOUTlog lines now state the budget they exhausted instead of printingasyncio.wait_for's empty message. - cache: rejected credentials during the domain staleness probe now report the domain as stale and log at
errorlevel, instead of falling through the generic fail-open handler and reporting it fresh. Unlike a transient probe error, a bad password or API key recurs on every tick and the nightly force job cannot repair it either, so it is now routed into the reload path where it surfaces as a countabledomain_failedevent. The handler catchesInvalidCredentialsErroronly — transient login refusals such as Check Point'sDatabase revision is in progress(a plainAuthenticationError) and every other probe failure keep the existing fail-open behaviour. - asdk:
LoginCoordinatornow raisesInvalidCredentialsError(an existing, previously unused subclass ofAuthenticationError) when Check Point rejects the password or API key, and plainAuthenticationErrorfor every other login refusal, so callers can distinguish a permanent credential problem from a transient one by type instead of by matching on message text. The marker string is exported asarodonata.config.CREDENTIAL_REJECTION_MESSAGE. Backwards compatible: existingexcept AuthenticationErrorhandlers still catch the subclass. - asdk:
RateLimiter.acquire()is now a true semaphore over itsconcurrent_limitslots — it starts at the hashed slot but takes any free one, and only waits (up toslot_timeout) when every slot is busy. Previously each task was pinned to a single hashed slot with no fallback, so a login could starve for the full 90 s behind one long-running call on that slot while the other slots sat idle; observed in integration asLockAcquisitionError: ratelimit:<ip>:slot_2during rapid logout/login cycles.DatabaseLockManagergained the non-blockingtry_acquire_lock()this requires. - asdk: overlapping keepalive sweeps (
LoginCoordinator.maintain_keepalives) now coalesce — a sweep that finds one already in flight returns immediately instead of launching another swarm of slot-holding keepalive tasks. Previously everyapi_callfired its own sweep, so a burst of calls against a throttled server multiplied rate-limiter contention. - tests: every integration revert now routes through one helper carrying the 900 s
REVERT_TIMEOUT_SECONDS, instead of individual call sites using the client's default API timeout; a unit test enforces that no integration test issuesrevert-to-revisiondirectly.
v1.8.0 (2026-09-04)¶
Feat¶
- cpcrud: resolve NAT sentinel UIDs at runtime, cached per management server
- cpcrud: export NAT_ANY_OBJECT_UID for reuse by external consumers
- domains: opt-in include_global on domain readers and refresh paths
- domains: write an explicit Global domain row on multi-domain managers
Fix¶
- NAT translated-* writes use the Original UID; domain list re-fetch gap
- backfill Global domain row on already-provisioned MDMs, gate on positive MDM detection
Docs¶
- Added
docs/scripts/build_notebooklm_docs.py, generating a three-filedocs-notebooklm/bundle (concepts/guide, full API reference auto-extracted from source viaast, and examples with resolved snippets) sized for upload to NotebookLM or similar AI document-chat tools. - Documented CPCRUD's section/layer-relative rule positioning (
docs/user-guide/cpcrud.md), including cleanup-rule-awarebottomplacement for access/HTTPS/threat-prevention layers.
v1.7.0 (2026-08-21)¶
Feat¶
- incremental refresh mode with show-changes diff and full object re-fetch
v1.6.0 (2026-08-19)¶
Feat¶
- cache: atomic replace_domain_objects (single-transaction delete+insert)
- helpers: rule getters accept and propagate cache_mode/cache_ttl
- Initial public release as Arodonata — renamed from the previously-internal
cpaiopsproject, with a consistent identifier rename throughout (ArodonataClient/ArodonataSettings,ARODONATA_*environment variables,arodonata.*OpenTelemetry span namespace). - Added
examples/08_otel_smart_refresh.py, demonstrating how a standalone script wires up OpenTelemetry tracing itself viaarlogi.otel.setup_tracing()and printing a per-span timing breakdown.
Fix¶
- cache: collect-then-swap domain refresh; abort on partial failure without touching cache or freshness stamp
- client: get_gateways inner orchestration reads bypass object-cache coordinator as documented
- helpers: get_gateways no longer triggers object-cache coordinator
- reclaim stale read-write sessions with pending changes, configurable rate-limit slot timeout
client.cpcrud.apply()'s outcome summary now correctly reports areuseoutcome for auto-created dependencies (e.g. an implicitly-created service referenced by an access rule) that already exist on re-apply, instead of omitting them.
Docs¶
- Corrected the CPCRUD operation-type reference table (
docs/user-guide/cpcrud.md) and the CPCRUD settings defaults (docs/configuration/index.md) to match the actual schema and code. - Added narrated example pages for
07_crud_inverse.py,07_session_basics.py, and08_otel_smart_refresh.py.