Repository navigation
Releases: unc-network/akips
Release list
v1.4.0
Added
-
get_snmp_state(), the SNMP counterpart toget_ping_state(). Same record,
same shape: the agent's state, when the device was added, and when the state
last changed.It answers for far fewer devices, and that is the point of the docstring.
AKiPS pings everything it holds but polls SNMP only where SNMP is
configured. Measured on one fleet, 5,821 devices of 16,785 appeared here and
the rest were ICMP only — so a caller assuming the same denominator as the
ping call reads two thirds of the fleet as broken. Absence means not polled,
not down.
Documentation
get_unreachable()records that asking only for what is broken is not
necessarily the cheap way. On one fleet it took roughly three times as long
asget_ping_state()while returning 289 times fewer rows. Whether that
holds elsewhere is unknown and it may be a property of that server's data,
so the note says to measure rather than to switch — but the opposite is the
natural assumption, which is why it is written down.
v1.3.0
Added
-
get_ping_state(), the ping enum for every device rather than only the ones
that are down.get_unreachable()answers "what is broken now" and filters
todown, which left the same record unreachable for a healthy device —
0.3% of a campus, and the least interesting 0.3%. This is the same query
without the filter, and returns the whole fleet in about a second.The two epochs it carries are the point, and their names undersell them.
createdis when AKiPS started polling, so it is the date the device was
added and is not otherwise available for a device that is up.modifiedis
the instant the state last changed, not a row-touched timestamp. Both arrive
as aware datetimes in the server's timezone rather than as the integers the
raw attribute holds.Together they answer the column AKiPS shows on its own device dashboard, the
one reading Uptime on a device that is up and Downtime on one that is down:
the figure is now minusmodified, and the state decides the word. That is
notsysUpTime, which counts from the last boot and keeps counting through
an outage, so the two disagree on exactly the devices somebody is looking at.
Documentation
get_attributes()says that a bare attribute name matches exactly and so
usually matches nothing: AKiPS qualifies most attributes with their MIB, and
sysUpTimefinds none of the devicesSNMPv2-MIB.sysUpTimedoes. The
unqualified form is not an error — it returns an empty result, which reads
as a fleet where nothing reports that attribute.
v1.2.0
Adds the module's first destructive call, and repairs three methods that
1.1.0 broke.
Anyone on 1.1.0 should take this. Moving the password into a POST body
made every api-script call hang, so get_device_by_ip(),
set_group_membership() and delete_device() have been unusable since
2026-08-13. Nothing about the symptom names the cause.
Added
-
delete_device(), the module's first destructive call. It removes one device
and cannot be undone. Whether the samples, events and availability held
against it go too is decided by AKiPS'sconfig_delete_devicebuilt-in,
which the site script calls and which this module cannot see into, so treat
the whole record as lost.It needs
web_delete_devicefrom AKiPS's site scripts page, which is not
installed by default. See akips_setup/README.md.The safety is most of the method. It takes one exact name and refuses a
pattern, an asterisk, or a name containing a comma — the site script reads
device_namesas one parameter and splits it on commas itself, so a comma
is a second device rather than an odd name, and an oversized delete cannot
be walked back. A missingrw_passwordis refused before anything is looked
up.The script prints nothing whether it worked or not, so the method confirms
the outcome instead of trusting the silence: it checks the device is there
first and that it is gone afterwards. That is two extra requests, which is
the right trade for an operation with no undo. It returnsTruewhen a
device was deleted andFalsewhen there was no such device, so a caller
does not report success for a name that never existed, and raises
AkipsErrorif the device survives the call.There is no merge. Where one box is registered twice under two names,
whatever the surviving record should keep has to be copied across before the
other is deleted.It waits
SCRIPT_TIMEOUTseconds, 300 by default, rather than the
client's timeout, and takes atimeoutargument of its own. A timeout part
way through a destructive call leaves the outcome unreadable, and waiting
longer costs only waiting. A client configured with something longer keeps
it — this is a floor, not a ceiling. The two lookups either side are
ordinary reads and use the client's timeout.SCRIPT_TIMEOUTis deliberately not named for the delete. Every site
script that goes away and does work wants the same thing — more room than a
read gets — so discovery, rewalk and rename would use this rather than each
bringing a constant of its own. Site scripts that answer at once,
get_device_by_ip()andset_group_membership(), keep the client's
timeout, so a hung lookup still fails promptly._get()takes a per-request
timeoutas well.An exception from it does not mean nothing happened. The confirmation
cannot run when the call itself fails, and AKiPS finishes the work whether
or not the client is still listening — the delete above raisedRead timed outand removed the device anyway. On any exception, ask AKiPS again rather
than recording a failure.
Fixed
-
api-scriptis now sent as GET, so the three site script methods work
again. That section does not answer a POST: it returns 200 headers, then
no body, and holds the connection open until the client gives up. Every call
toget_device_by_ip(),set_group_membership()anddelete_device()has
hung since 1.1.0 moved the module to POST. Reported to AKiPS 2026-08-21.api-dbaccepts a POST with the identical header, so this isapi-script
specifically rather than anything wrong with the request.The cost is that those three calls put the password back in the query
string, which is exactly whatuse_postexists to prevent, and it is the
api-rwpassword for two of them. There is no third option — the
alternative is a call that never returns — but anyone who moved to POST for
the security reason should know they are not getting it here.The verb is chosen per section from
SECTION_METHODS, a class attribute
besideSECTION_USERS. It is a class attribute so that a server behaving
differently from the ones seen here can be accommodated without waiting for
a release:AKIPS.SECTION_METHODS['api-script'] = 'POST' # if a server takes it
Sending the password in a POST body is itself undocumented, given out by
AKiPS support rather than described in the API guide, so what a particular
server accepts is best treated as a property of that server.use_post=Falsestill sends everything as GET, as before.
v1.1.0
Added
-
AkipsAuthenticationErrorwhen AKiPS rejects the username and password, and
AkipsSectionDisabledErrorwhen the section is switched off on the server.
Both are ordinary first-run mistakes, and both used to arrive as a bare
AkipsErrorcarrying only whatever AKiPS said.Both subclass
AkipsError, so code that catches that is unaffected. The
wording they match on is undocumented, so an error AKiPS phrases some other
way still raisesAkipsErrorrather than being forced into a category.Both carry what the call already knew, so nothing has to parse the message:
.sectionon each, and.usernameonAkipsAuthenticationError— which is
usually the useful half, since a section needingapi-rwand givenapi-ro
fails there rather than anywhere more obvious. Both come from the request,
not from the reply, so they stay right if AKiPS rewords it. There is no HTTP
status worth carrying: AKiPS answers 200 to everything, errors included. -
use_postonAKIPS(), defaultTrue. See below.
Security
-
The password is now sent in a POST body instead of the query string. A URL
is routinely recorded by web servers, proxies and load balancers in their
access logs, and turns up in exception messages and client history; a
request body is not. Credentials have no business in a URL, and this one had
been in every request the module made. Verified against AKiPS 26.5 across
all ten API sections and both accounts.Pass
use_post=Falseto send the previous GET form, for a server that will
not accept a POST. Nothing falls back on its own: a silent retry over GET
would put the password back in the URL at exactly the moment the server
turned out not to support the fix.Nothing else moves. The username and every other parameter stay in the query
string, no method signature changes, and callers see no difference.Tests that mock
requests.Session.getwill no longer intercept. Mock
postas well, or passuse_post=False. Nothing announces this: the patch
simply stops matching and the call is attempted for real, so a suite that
passed yesterday fails on a connection error to a host it thought was faked.
On a machine where that hostname resolves, the request is sent. This is also
in the README, because a changelog does not ship in the wheel and this is
the one note here that a consumer needs before they upgrade rather than
after.
Documentation
get_unreachable()is documented in the README, with the shape it returns
and three things that were only discoverable from the source: when a device
fails both checks the ping line supplieschild,index,device_added
andip4addr, because it is the only line carrying an address;'n/a'
means a check did not report as down, since the query asks only for
failing checks; and the pair of states reads as a progression, because a
device under load stops answering SNMP before it stops answering ping.
v1.0.0
The first release to commit to a stable API. Return shapes that disagreed with
each other are settled, the public methods are annotated, and the package ships
a py.typed marker, so what a caller gets back is now something a type checker
can see rather than something to be discovered at runtime.
Those corrections are breaking, and they are gathered here deliberately: better
to face them once than to meet them one at a time across several releases. From
here, a breaking change means a 2.0.
Developed and validated against AKiPS v26.5, with each pre-release
exercised against a live fleet of roughly 17,000 devices. No lower bound is
claimed. AKiPS's current published API guide is edition 17, covering release 22.10 of
December 2022 and unrevised across the twenty five releases since, so where it
and a running server disagree this release follows the server.
Upgrading: see MIGRATING.md for what to change, with before
and after for each one. Two account for most of the work:
# the client takes a password per account rather than one for all of them
AKIPS('akips.example.com', password=pw) # was
AKIPS('akips.example.com', ro_password=pw) # is
# nothing found is None everywhere, not an empty container
if api.get_unreachable() == {}: # was
if api.get_unreachable() is None: # is, and `if not down:` covers bothAdded
-
ro_passwordandrw_passwordon the client, and each call now
authenticates as the account its API section requires. AKiPS ships two API
accounts and its sections do not all accept the same one:api-script
needsapi-rw,api-msgneedsapi-ro, andapi-dbtakes either. A
single client could therefore never reach the whole API, and callers had to
construct two of them and remember which was which.api = AKIPS('akips.example.com', ro_password='...', rw_password='...')
Where either account will do, the read only one is preferred. A section
needing an account whose password was not supplied raises
AkipsCredentialErrorbefore the request is made, naming the section, the
account and the argument to pass, rather than letting AKiPS reject it.
call()takesuser='ro'or
user='rw'for the sections this module does not model and for a command
needing more rights than its section usually does.usernameandpasswordstill work: withapi-roorapi-rwthey fill
that account, and with any other name that pair is used for every section,
which is how a custom AKiPS API account will work once AKiPS offers them.
Constructing a client with no password at all is now refused. Breaking
for that case, which could never have authenticated anyway. -
A warning when a call names an API section AKiPS does not publish, which is
usually a typo.SECTION_USERSnow lists all ten documented sections with
the account each one accepts, taken from the server's own Web API settings
page, so it serves as both the account mapping and the list of what exists.
Every section takesapi-roexceptapi-script, which requiresapi-rw,
andapi-db, which takes either. Unknown
sections are warned about rather than refused, because AKiPS may add
sections andcall()exists so that reaching one need not wait for a
release here. Each section is warned about once per client, so a poll loop
does not fill a log. -
verifyaccepts a path to a CA bundle as well asTrueorFalse, which
requests has always supported but the annotation and documentation did not
mention. This is how to trust a server whose certificate chain is missing an
intermediate without turning verification off entirely. -
Type annotations on every public method, and an
akips/py.typedmarker so
consumers type checking their own code see real types instead ofAny.
The annotations are checked with mypy in CI, so what the marker promises is
verified here rather than discovered downstream. -
timeouton the client, defaulting to the 30 seconds already in use. It can
be changed on an existing client withapi.timeout = 60. Previously_get
accepted a timeout that nothing forwarded, so callers had to subclass to
reach it. -
get_group_availability()— availability statistics for a group over a time
period, from theapi-availabilitysection. -
get_latest_values()— the most recent reading of a numeric attribute for
each device. Counters and gauges keep no reading in the config database that
get_attributes()reads; that holds their type definition, which is the same
for every device. The readings are in the time series database, so this asks
for a short series and keeps the last value in it.api.get_latest_values('UPS-MIB.upsBatteryVoltage', child='battery')
The final interval of a series is usually still being filled and comes back
empty, so the last column is not the answer; this returns the last column
that has a value, with when it was measured. Results are keyed by device and
child, because an attribute like interface utilization has one reading per
interface. Values arrive already scaled by AKiPS, in the attribute's real
units. -
get_ups_battery_status()— which UPS batteries are not reporting as
normal, defaulting tounknown,batteryLowandbatteryDepleted. This is
the battery's own condition, which UPS-MIB reports separately from where the
UPS draws its output. Note that the numeric readings such as estimated
minutes remaining are not available this way: AKiPS keeps those in its time
series database, and reading them withmgetreturns the gauge's scaling
factor, which is the same for every device. -
get_ups_output_source()— which UPS devices are not running on mains
power. Returns every source butnormalby default, since that is the list
worth acting on; passstates=Nonefor every UPS whatever its state. That
includesnone, a UPS delivering no output at all, andother, one that
cannot classify its own source — a UPS that cannot answer the question is
worth looking at for the same reasonunknownis in the battery states.
Note this is the output source rather than the battery's own health, which
UPS-MIB reports separately asupsBatteryStatus. -
get_liebert_battery_test()— the result of the last UPS battery self test,
defaulting to failures only. The vendor is in the name deliberately:
battery test results are not in the standard UPS-MIB, so this reads an
attribute only Liebert and Vertiv equipment reports, and against another
vendor's fleet it would return nothing, which reads as good news. Another
vendor's equivalent attribute can be passed to reuse the same parsing.Both return the parsed enum, so a caller gets the state and when it last
changed rather than a raw string. These are the first callers of the enum
parser, which had been kept unused pending evidence that the format
generalized beyond ping and SNMP state; it does. -
get_aggregate(labeled=True)— a time against each aggregate value, as a
list of{'time', 'value'}dictionaries withtimetimezone aware and
valuea float. An aggregate arrives from AKiPS as bare numbers with no
timestamps, unlike the series commands, so this asks the server for the
window withtf pairsand spaces the values across it: one extra request,
and the only honest way to do it, since working the axis out here would use
this module's clock rather than the server's. The default isFalseand the
unlabeled call is unchanged, making one request as before.A period covering several disjoint ranges, such as
'lastweek; mon to fri 8:00 to 17:00', raisesValueErrorrather than
drawing five weekday windows as one continuous axis. An interval with no
reading is kept withNonerather than dropped, since dropping a point
would shift every later one along the axis — and a calendar relative period
produces a great many of those,last1dbeing the whole of today, so only
the elapsed intervals hold anything. Those are logged at debug rather than
warned about; the warning is reserved for a value that is present but not a
number, and names it. -
get_syslog()andget_traps()— the two message types by name, with every
filterget_msg()takes forwarded.get_msg()still reaches both at once,
but a caller wanting one type no longer has to spell it: an enum you never
type is an enum you cannot mistype, which is the failuremsg_type
validation now catches from the other direction. -
get_device_availability()andget_event_availability()— availability
per device and child, and the up and down event pairs behind those totals.
Both existed as commented-outpassstubs; they are implemented now that
the parameters and column shapes have been confirmed against a live
server. Each mode ofnm-availabilityreturns its own columns, so the
three are parsed separately rather than sharing a field list — events mode
in particular returnsdownandupwhere group mode returnsattrand
a group name.All three availability methods default to
period='last24h', a rolling 24
hours, rather thanlast1d. AKiPS readslastNdas N-1 whole days plus
today so far, solast1dis today rather than a day: measured against a
live server at 09:50 it was 35,458 seconds, andlast7dwas 553,860, six
whole days plus today. An availability percentage is a proportion of the
window it was measured over, so the old default shrank to minutes just
after midnight and would have read as a confident 100% every night with
nothing in the reply announcing it.lastNhandlastNmare rolling
windows of the length they name, andtotal timein every reply is the
window actually measured.Both
get_device_availability()andget_event_availability()require a
deviceor agroup. AKiPS answers an unscoped call in
either mode with an empty body rather than an error, which w...
v0.6.0
A maintenance release covering supported Python versions, dependency
security, and release automation. The client's own code is unchanged from
0.5.1 apart from the version string: every public method has the same
signature and behavior. The only difference an installer sees is the
raised Python floor.
Prototype api-availability methods remain disabled pending further
testing, so they are not part of this release.
Added
- Python 3.14 to the supported and tested versions.
- Automatic tagging, GitHub release creation, and PyPI publishing when a
new version reachesmain. Release notes are taken from this file, and
pre-release versions are refused on every route into the production
publish. - A security audit workflow running
pip-auditagainst the locked
dependencies on changes and weekly, plus ruff'sSrules, which port
bandit's static security checks into the existing lint pass. - This changelog, backfilled from git history and release tags.
AI.md, recording how AI tooling is used in this project and what it
has and has not touched.
Changed
- Updated all locked dependencies, clearing eleven security advisories.
Every remaining fix required Python 3.10 or newer, which is what
prompted the version floor below. - Replaced pylama with ruff for linting. pylama has been unreleased since
2022 and depends onpkg_resources, which recent setuptools removed. The
same rules carry over: pycodestyle, pyflakes, and a 128 character line
length.setup.cfgis gone, since it only held pylama's configuration. - Replaced pdoc3 with pdoc for API documentation. pdoc3 has been unreleased
since 2021 and importsdistutils, which left the standard library in
3.12. The module page moves fromdocs/akips/index.htmlto
docs/akips.html; the previous path redirects, so existing links keep
working. - API documentation is now generated and published by CI on every push to
main, rather than being generated by hand and committed. The published
site tracksmainand no longer followsdevelop. - Publishing now uses separate
pypiandtestpypienvironments instead
of one shared environment namedprod, so production releases and
TestPyPI rehearsals can be governed independently. - CI runs on pinned runner images rather than
-latest, so moving to a
new Ubuntu is a deliberate change rather than one that arrives on
GitHub's schedule.
Removed
- Python 3.9 support. The minimum supported version is now 3.10.
Breaking for anyone still on 3.9, which reached end of life in
October 2025. - All long-lived PyPI and TestPyPI API tokens. Publishing is entirely
through trusted publishing, so the project stores no credential that
could leak or need rotating.