Skip to content

Latest commit

 

History

History
262 lines (193 loc) · 11.5 KB

File metadata and controls

262 lines (193 loc) · 11.5 KB

elm - CLAUDE.md

What this project is

elm is a CLI tool that queries the LogicMonitor REST API and outputs data in multiple formats: CSV, JSON, HTML, Markdown, XML, LaTeX, RST, tab, jira, raw, txt, and api. It uses Click for the CLI framework and pandas for output formatting.

CRITICAL: Code generation architecture

Do not edit generated files directly.

The build pipeline is:

  1. make init -- downloads the LM Swagger JSON spec, splits it into per-endpoint definition files in _defs/
  2. make render -- runs Jinja2 templates from _jnja/ against those definitions to generate Python source files in _cmds/
  3. make render also generates _cmds/init.py (via touch) so PyInstaller can treat _cmds/ as a proper package and collect it with --collect-all
  4. elm.py uses LazyGroup to defer _cmds/ module imports until a subcommand is actually invoked -- --version, --help, and --list load without importing any _cmds/ module or heavy library (pandas, requests, etc.)

The source of truth for command logic is _jnja/ (templates) and _defs/ (per-endpoint JSON definitions). If you fix a bug in a _cmds/ file, the fix will be overwritten next time make render runs. Fix it in the template instead.

Directory layout

_jnja/                     Jinja2 templates -- EDIT THESE, not the output
_defs/                     Per-endpoint JSON definitions (generated by make init)
_cmds/                     Generated Python command modules (DO NOT EDIT)
_build/                    PyInstaller build artefacts (DO NOT EDIT)
_dist/                     Compiled binary output (DO NOT EDIT)
elm.py                     Main entry point -- imports from _cmds/
setup.py                   Package setup
requirements.txt           Python dependencies
Makefile                   Build orchestration -- source of truth for build steps
swagger.undocumented.json  Snapshot of LM Swagger spec (checked in)
config.example.ini         Example credentials config

Build commands

make            Full build (init + render + cfg)
make init       Download swagger, create _defs/
make render     Generate _cmds/ from _jnja/ templates
make install    Build binary via PyInstaller into _dist/
make clean      Remove all generated files
make test       Quick offline tests
make testbasic  Test CLI flags (mostly offline; the -f api/curl/wget/sqlite
                assertions send a real request and need the live config profile)
make testverb   Test verbose flags (requires LM connection)

Dev loop: make clean && make && make install

(make install already copies both the elm executable and its _internal/ dependency dir into ~/bin — see the $(bindir)/$(name) recipe — so no manual cp afterwards is needed.)

Sandbox note: in a Linux sandbox with the host tree mounted directly, running make writes a Linux binary into _dist/ (and regenerates _cmds/, engine.py, elm.py, etc.). These are gitignored, so nothing is committed, but they overwrite the host's macOS build — _dist/elm/elm will then fail on macOS with "cannot execute binary file". Rebuild on the Mac (make clean && make && make install) to restore it. Also note make re-downloads the documented swagger from logicmonitor.com; if that host is firewalled a 127-byte block page silently clobbers _defs/swagger.json and the build proceeds with only the ~15 undocumented commands. To build from source in a sandbox without touching the host venv/ (which is macOS-arch), point make at an isolated venv: make VENV=/path/to/lxvenv JINJA=/path/to/lxvenv/bin/jinja2 (PyInstaller on Linux additionally needs binutils and a shared libpython).

Testing

There is no pytest/unittest suite — ignore the generic pytest guidance in the parent ../CLAUDE.md. Tests are Makefile-driven and run against the built PyInstaller binary (_dist/elm/elm):

  • make testbasic is mostly offline — CLI flags, help/version, --list, --ai, profile/config resolution, and the per-command <cmd> --help loop never contact LM. But its -f api, -f curl, -f wget and -f sqlite assertions are NOT offline: those formats print the URL/command only after a successful response.raise_for_status() + response.json(), so they send a real request and need the live default config profile (with dummy creds they 403 and fail). Run it after any template change, but expect those lines to need LM. (The old claim that they "build the URL without sending it" is wrong — the request is sent first.)
  • make testfmtcontent asserts each output format really is that format. The api/curl/wget assertions also send a real request (same reason as above); the rest hit LM via MetricsUsage.
  • Targets marked "(connects to LM)" (testfmts, testcount, testtotal, testverb, testid, testH/I, testhead/foot) need live credentials and the default config profile.

Run make test for the quick suite; make testlong for the slow ones.

Runtime

  • Uses a Python venv at venv/ (created by make)
  • Config file: ~/.config/logicmonitor/credentials/config.ini (unrepr format, see configobj docs)
  • Required config keys: access_id, access_key, account_name
  • Shell completion: elm-completion.bash is a generated file (rendered from _jnja/elm-completion.bash.j2 by make render). It is not committed to the repo. make completion installs it to $XDG_DATA_HOME/bash-completion/completions/elm (default ~/.local/share/bash-completion/completions/elm). bash-completion 2.x sources it automatically.

Credential profiles

elm supports multiple credential profiles:

  • --profile NAME resolves to ~/.config/logicmonitor/credentials/<NAME>.ini
  • --config PATH accepts a full path to any .ini file in any directory
  • elm --list (or elm -l) lists all available profiles and exits, marking the active one with *. Use this instead of reading the credentials directory.

Profile naming convention:

  • config.ini is always the default and should always point to the safest environment (sandbox/test). Do not put production credentials here.
  • Non-default environments get explicit names (e.g. preprod.ini, prod.ini) and must be selected deliberately with --profile.
  • config.example.ini is not a real profile — it is skipped by elm --list.

At the start of any session involving live API calls:

  1. Run elm --list to see what profiles are available and which is active.

  2. For testing, development, and learning: always use the default config profile (i.e. no --profile flag).

  3. For audits or cross-platform work: run elm --list, then ask "Which platform do you want to check?" and pass --profile NAME for each non-default environment. Example: elm --profile preprod AuditLogList -s0 ...

  4. Never read the contents of .ini files — they contain credentials. Only elm --list to discover what profiles are available.

LM API

  • REST API v2 (v1 also supported via apiversion=1 make variable)
  • Auth: HMAC-SHA256 signed requests, not OAuth
  • Base URL: https://{account_name}.logicmonitor.com/santaba/rest
  • Swagger spec: fetched fresh by make init, snapshot in swagger.undocumented.json

LM API quirks

AuditLogList: username "(update)" is a literal credential value

If you see audit log entries where username is the literal string (update), that is the actual access_id value configured in whatever tool or integration is making those requests. LM logs the access_id as the username — it does not substitute or redact deleted tokens.

Confirmed by test: running elm -i "(update)" -k "..." DeviceList produced an audit log entry with username: "(update)" immediately.

This means: somewhere in the target portal, a script or integration has access_id = (update) (likely a misconfigured or placeholder credential). Track down the source by looking at the IP field in the audit log entry and the access path in the description:

elm AuditLogList -F 'username:(update)' -s0

The ip field will show the originating host; the description will show which API path it is hitting.

Output formats

csv, tsv, html, prettyhtml, jira, json, jsonl, prettyjson, xml, prettyxml, latex, md, rst, gfm, pipe, tab, raw, txt, api (api makes the request then prints the URL and Authorization header — header contains HMAC signature, do not share)

Dependencies

Key packages: click, pandas, requests, configobj, pyinstaller, jinja2. See requirements.txt for pinned versions.

Known confusion points for AI tools

  • engine.py, elm.py, and elm-completion.bash are all generated files, rendered from _jnja/engine.py.j2, _jnja/elm.py.j2, and _jnja/elm-completion.bash.j2 respectively. None are committed to the repo. Fix bugs in the _jnja/ templates, not in the generated files directly.

  • _cmds/*.py files look like source but are generated artefacts. Linters and type checkers will flag issues in them that should be fixed in _jnja/ templates, not in the files themselves.

  • elm.py uses LazyGroup to dynamically import _cmds/ modules via importlib.import_module() at dispatch time. Static analysis will show import errors that are not real errors. PyInstaller requires --collect-all=_cmds and _cmds/init.py to bundle them correctly.

  • The Makefile does a lot of heavy lifting. Read it before assuming something is missing from the Python code.

  • swagger.undocumented.json contains endpoints not in the official LM docs. This is intentional.

Current state

The project builds and runs. It uses the pip + venv workflow (a past partial Poetry migration was abandoned and removed — do not reintroduce Poetry unless doing a deliberate packaging migration).

Environment notes:

  • Python 3.14 is validated: a full clean build runs cleanly on 3.14.6 with the current pins (pandas 2.3.3, numpy 2.5.0, pyinstaller 6.21.0). If a future dependency bump surfaces incompatibilities, fall back to a conservative interpreter with make PYTHON=python3.12.
  • setup.py ships the generated top-level modules via py_modules=['elm', 'engine', '_version'] (not find_packages()).

Resolved (kept here so the history is not re-investigated):

  • Slow startup — fixed in v1.8.0 via LazyGroup + deferred heavy imports (--version now loads in ~0.2s).
  • make -n infinite recursion / duplicated build from a recursive $(MAKE) in the _defs/commands.json recipe — fixed; the build now calls explicit sub-targets ($(MAKE) _render _build).
  • Query params are sent structured (requests.get(..., params=...) in _jnja/engine.py.j2), not hand-concatenated.

Removed:

  • -x / --export (export a query as a standalone script) was a never-finished stub carried over from an older project — its handler referenced an elm.flags attribute that was never set, used jinja2 symbols it never imported, and rendered a template that did not exist, so the advertised flag crashed on use. It was removed from _jnja/elm.py.j2, _jnja/engine.py.j2, and the README. The reproduce -a-request need it was meant to serve is already covered by -f api (prints the exact request URL + auth header) plus the PyInstaller binary.

Deferred work lives in todo.md. Agreed follow-ups from the 2026-07 audit live in RECOMMENDATIONS.md — work items are written there in full so any assistant can execute them; do them in order, one at a time.

Do not

  • Edit files in _cmds/ directly
  • Edit files in _defs/ directly
  • Edit files in _build/ or _dist/
  • Edit elm-completion.bash directly — it is generated from _jnja/elm-completion.bash.j2
  • Regenerate _cmds/ and commit without also committing the template change in _jnja/ that produced them