Skip to content

Releases: unc-network/akips

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 23 Aug 16:16
dc7ec17

Added

  • get_snmp_state(), the SNMP counterpart to get_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
    as get_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

Choose a tag to compare

@github-actions github-actions released this 23 Aug 15:35
0eb7920

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
    to down, 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.
    created is when AKiPS started polling, so it is the date the device was
    added and is not otherwise available for a device that is up. modified is
    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 minus modified, and the state decides the word. That is
    not sysUpTime, 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
    sysUpTime finds none of the devices SNMPv2-MIB.sysUpTime does. 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

Choose a tag to compare

@github-actions github-actions released this 21 Aug 20:21
32bb415

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's config_delete_device built-in,
    which the site script calls and which this module cannot see into, so treat
    the whole record as lost.

    It needs web_delete_device from 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_names as 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 missing rw_password is 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 returns True when a
    device was deleted and False when there was no such device, so a caller
    does not report success for a name that never existed, and raises
    AkipsError if 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_TIMEOUT seconds, 300 by default, rather than the
    client's timeout, and takes a timeout argument 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_TIMEOUT is 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() and set_group_membership(), keep the client's
    timeout, so a hung lookup still fails promptly. _get() takes a per-request
    timeout as 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 raised Read timed out and removed the device anyway. On any exception, ask AKiPS again rather
    than recording a failure.

Fixed

  • api-script is 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
    to get_device_by_ip(), set_group_membership() and delete_device() has
    hung since 1.1.0 moved the module to POST. Reported to AKiPS 2026-08-21.

    api-db accepts a POST with the identical header, so this is api-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 what use_post exists to prevent, and it is the
    api-rw password 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
    beside SECTION_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=False still sends everything as GET, as before.

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 13 Aug 13:28
40a88d7

Added

  • AkipsAuthenticationError when AKiPS rejects the username and password, and
    AkipsSectionDisabledError when the section is switched off on the server.
    Both are ordinary first-run mistakes, and both used to arrive as a bare
    AkipsError carrying 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 raises AkipsError rather than being forced into a category.

    Both carry what the call already knew, so nothing has to parse the message:
    .section on each, and .username on AkipsAuthenticationError — which is
    usually the useful half, since a section needing api-rw and given api-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_post on AKIPS(), default True. 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=False to 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.get will no longer intercept. Mock
    post as well, or pass use_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 supplies child, index, device_added
    and ip4addr, 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

Choose a tag to compare

@github-actions github-actions released this 03 Aug 16:52
8486500

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 both

Added

  • ro_password and rw_password on 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
    needs api-rw, api-msg needs api-ro, and api-db takes 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
    AkipsCredentialError before the request is made, naming the section, the
    account and the argument to pass, rather than letting AKiPS reject it.
    call() takes user='ro' or
    user='rw' for the sections this module does not model and for a command
    needing more rights than its section usually does.

    username and password still work: with api-ro or api-rw they 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_USERS now 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 takes api-ro except api-script, which requires api-rw,
    and api-db, which takes either. Unknown
    sections are warned about rather than refused, because AKiPS may add
    sections and call() 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.

  • verify accepts a path to a CA bundle as well as True or False, 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.typed marker so
    consumers type checking their own code see real types instead of Any.
    The annotations are checked with mypy in CI, so what the marker promises is
    verified here rather than discovered downstream.

  • timeout on the client, defaulting to the 30 seconds already in use. It can
    be changed on an existing client with api.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 the api-availability section.

  • 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 to unknown, batteryLow and batteryDepleted. 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 with mget returns 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 but normal by default, since that is the list
    worth acting on; pass states=None for every UPS whatever its state. That
    includes none, a UPS delivering no output at all, and other, one that
    cannot classify its own source — a UPS that cannot answer the question is
    worth looking at for the same reason unknown is in the battery states.
    Note this is the output source rather than the battery's own health, which
    UPS-MIB reports separately as upsBatteryStatus.

  • 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 with time timezone aware and
    value a float. An aggregate arrives from AKiPS as bare numbers with no
    timestamps, unlike the series commands, so this asks the server for the
    window with tf pairs and 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 is False and 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', raises ValueError rather than
    drawing five weekday windows as one continuous axis. An interval with no
    reading is kept with None rather 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, last1d being 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() and get_traps() — the two message types by name, with every
    filter get_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 failure msg_type
    validation now catches from the other direction.

  • get_device_availability() and get_event_availability() — availability
    per device and child, and the up and down event pairs behind those totals.
    Both existed as commented-out pass stubs; they are implemented now that
    the parameters and column shapes have been confirmed against a live
    server. Each mode of nm-availability returns its own columns, so the
    three are parsed separately rather than sharing a field list — events mode
    in particular returns down and up where group mode returns attr and
    a group name.

    All three availability methods default to period='last24h', a rolling 24
    hours, rather than last1d. AKiPS reads lastNd as N-1 whole days plus
    today so far, so last1d is today rather than a day: measured against a
    live server at 09:50 it was 35,458 seconds, and last7d was 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. lastNh and lastNm are rolling
    windows of the length they name, and total time in every reply is the
    window actually measured.

    Both get_device_availability() and get_event_availability() require a
    device or a group. AKiPS answers an unscoped call in
    either mode with an empty body rather than an error, which w...

Read more

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 31 Jul 19:02
8931d6a

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 reaches main. 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-audit against the locked
    dependencies on changes and weekly, plus ruff's S rules, 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 on pkg_resources, which recent setuptools removed. The
    same rules carry over: pycodestyle, pyflakes, and a 128 character line
    length. setup.cfg is gone, since it only held pylama's configuration.
  • Replaced pdoc3 with pdoc for API documentation. pdoc3 has been unreleased
    since 2021 and imports distutils, which left the standard library in
    3.12. The module page moves from docs/akips/index.html to
    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 tracks main and no longer follows develop.
  • Publishing now uses separate pypi and testpypi environments instead
    of one shared environment named prod, 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.

v0.5.1

Choose a tag to compare

@wwhitaker wwhitaker released this 19 Nov 17:57
0ce6e7a

Add calls for syslog and trap message collection.

v0.5.0

Choose a tag to compare

@wwhitaker wwhitaker released this 14 Nov 18:33
7e0a47e

Online pdoc update and additional debug logging line with senstive param redaction. Added additional documentation around the various Web API section options.

v0.4.5

Choose a tag to compare

@wwhitaker wwhitaker released this 06 Oct 13:29
064057b

README and documentation updated along with poetry dependencies.

v0.4.4

Choose a tag to compare

@wwhitaker wwhitaker released this 02 Oct 22:50
56a3b36

Includes a minor fix for device group membership search and adds group filters for events searches.