ipmg 2.4.0__tar.gz → 3.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {ipmg-2.4.0 → ipmg-3.1.0}/PKG-INFO +142 -15
  2. ipmg-2.4.0/src/ipmg.egg-info/PKG-INFO → ipmg-3.1.0/README.md +132 -57
  3. {ipmg-2.4.0 → ipmg-3.1.0}/pyproject.toml +15 -7
  4. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/__init__.py +1 -1
  5. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/cli/commands.py +20 -2
  6. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/cli/config.py +5 -0
  7. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/cli/parser.py +30 -2
  8. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/core/diff.py +8 -4
  9. ipmg-3.1.0/src/ipmg/core/discovery.py +254 -0
  10. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/core/ping.py +37 -1
  11. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/core/portscan.py +6 -2
  12. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/infrastructure/database.py +17 -1
  13. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/infrastructure/file_io.py +180 -66
  14. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/infrastructure/incremental.py +57 -31
  15. ipmg-3.1.0/src/ipmg/reporting/frames.py +74 -0
  16. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/reporting/machine.py +5 -7
  17. ipmg-3.1.0/src/ipmg/reporting/metrics.py +125 -0
  18. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/reporting/summary.py +12 -9
  19. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/services/scan_service.py +9 -8
  20. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/app.py +36 -21
  21. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/server.py +7 -1
  22. ipmg-2.4.0/README.md → ipmg-3.1.0/src/ipmg.egg-info/PKG-INFO +184 -8
  23. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg.egg-info/SOURCES.txt +3 -0
  24. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg.egg-info/requires.txt +11 -6
  25. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_commands.py +39 -1
  26. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_config.py +16 -0
  27. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_file_io.py +61 -19
  28. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_incremental.py +7 -4
  29. ipmg-3.1.0/tests/test_ipv6.py +275 -0
  30. ipmg-3.1.0/tests/test_metrics.py +162 -0
  31. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_parser.py +20 -0
  32. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_scan_service.py +11 -8
  33. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_web_api.py +6 -5
  34. ipmg-2.4.0/src/ipmg/core/discovery.py +0 -75
  35. ipmg-2.4.0/src/ipmg/reporting/frames.py +0 -45
  36. {ipmg-2.4.0 → ipmg-3.1.0}/LICENSE +0 -0
  37. {ipmg-2.4.0 → ipmg-3.1.0}/setup.cfg +0 -0
  38. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/__main__.py +0 -0
  39. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/cli/__init__.py +0 -0
  40. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/core/__init__.py +0 -0
  41. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/core/engine.py +0 -0
  42. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/core/health.py +0 -0
  43. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/core/security.py +0 -0
  44. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/exceptions.py +0 -0
  45. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/infrastructure/__init__.py +0 -0
  46. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/infrastructure/notify.py +0 -0
  47. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/reporting/__init__.py +0 -0
  48. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/reporting/diff_report.py +0 -0
  49. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/reporting/live.py +0 -0
  50. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/reporting/ui.py +0 -0
  51. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/services/__init__.py +0 -0
  52. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/services/history_service.py +0 -0
  53. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/utils/__init__.py +0 -0
  54. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/utils/helpers.py +0 -0
  55. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/__init__.py +0 -0
  56. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/db.py +0 -0
  57. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/manager.py +0 -0
  58. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/schemas.py +0 -0
  59. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/static/css/app.css +0 -0
  60. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/static/index.html +0 -0
  61. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/static/js/api.js +0 -0
  62. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/static/js/app.js +0 -0
  63. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/static/js/charts.js +0 -0
  64. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/static/js/demo.js +0 -0
  65. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg/web/static/js/views.js +0 -0
  66. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg.egg-info/dependency_links.txt +0 -0
  67. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg.egg-info/entry_points.txt +0 -0
  68. {ipmg-2.4.0 → ipmg-3.1.0}/src/ipmg.egg-info/top_level.txt +0 -0
  69. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_api_contract.py +0 -0
  70. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_database_history.py +0 -0
  71. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_diff.py +0 -0
  72. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_diff_report.py +0 -0
  73. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_discover.py +0 -0
  74. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_engine.py +0 -0
  75. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_health.py +0 -0
  76. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_history_service.py +0 -0
  77. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_live.py +0 -0
  78. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_machine_output.py +0 -0
  79. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_notify.py +0 -0
  80. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_ping.py +0 -0
  81. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_ping_command.py +0 -0
  82. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_portscan.py +0 -0
  83. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_ui.py +0 -0
  84. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_utils.py +0 -0
  85. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_web_db.py +0 -0
  86. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_web_manager.py +0 -0
  87. {ipmg-2.4.0 → ipmg-3.1.0}/tests/test_web_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ipmg
3
- Version: 2.4.0
3
+ Version: 3.1.0
4
4
  Summary: IP Management & Ping Monitoring CLI Tool
5
5
  Author: Sameer Alam
6
6
  Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
@@ -29,16 +29,19 @@ Classifier: Environment :: Console
29
29
  Requires-Python: >=3.9
30
30
  Description-Content-Type: text/markdown
31
31
  License-File: LICENSE
32
- Requires-Dist: pandas>=2.2.2
33
32
  Requires-Dist: openpyxl>=3.1
34
33
  Requires-Dist: rich>=13.0
35
- Requires-Dist: fastapi>=0.110
36
- Requires-Dist: pydantic>=2.7
37
- Requires-Dist: uvicorn>=0.27
38
- Requires-Dist: websockets>=12
39
- Requires-Dist: python-multipart>=0.0.9
40
34
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
35
+ Provides-Extra: web
36
+ Requires-Dist: fastapi>=0.110; extra == "web"
37
+ Requires-Dist: pydantic>=2.7; extra == "web"
38
+ Requires-Dist: uvicorn>=0.27; extra == "web"
39
+ Requires-Dist: websockets>=12; extra == "web"
40
+ Requires-Dist: python-multipart>=0.0.9; extra == "web"
41
+ Provides-Extra: all
42
+ Requires-Dist: ipmg[web]; extra == "all"
41
43
  Provides-Extra: dev
44
+ Requires-Dist: ipmg[web]; extra == "dev"
42
45
  Requires-Dist: pytest>=7.4; extra == "dev"
43
46
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
44
47
  Requires-Dist: ruff>=0.4; extra == "dev"
@@ -151,11 +154,24 @@ The formula lives in
151
154
  and is updated with every release. Upgrade with `brew upgrade ipmg`, remove
152
155
  with `brew uninstall ipmg`.
153
156
 
154
- **Windows — two commands in PowerShell:**
157
+ **Windows — one command in PowerShell:**
155
158
 
156
159
  ```powershell
157
- powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
158
- uv tool install ipmg
160
+ irm https://raw.githubusercontent.com/sameeralam3127/ipmg/main/install.ps1 | iex
161
+ ```
162
+
163
+ It works in Windows PowerShell 5.1 and PowerShell 7 and needs no
164
+ administrator rights: it installs uv if it is missing, installs IPMG, adds it
165
+ to your user `PATH`, and checks that `ipmg --version` runs. To pin a release:
166
+
167
+ ```powershell
168
+ & ([scriptblock]::Create((irm https://raw.githubusercontent.com/sameeralam3127/ipmg/main/install.ps1))) -Version 2.3.0
169
+ ```
170
+
171
+ **Docker (amd64 and arm64):**
172
+
173
+ ```bash
174
+ docker run --rm ghcr.io/sameeralam3127/ipmg --input 10.0.0.0/24
159
175
  ```
160
176
 
161
177
  Then check it works, on any platform:
@@ -164,6 +180,38 @@ Then check it works, on any platform:
164
180
  ipmg --version
165
181
  ```
166
182
 
183
+ ### Running in Docker
184
+
185
+ The image carries `ping`, runs as an unprivileged user (uid 10001), and keeps
186
+ everything a scan writes (reports and the history database) in `/data`. Mount
187
+ a volume there to keep it between runs:
188
+
189
+ ```bash
190
+ docker run --rm -v ipmg-data:/data ghcr.io/sameeralam3127/ipmg --input 10.0.0.0/24 --compare
191
+ ```
192
+
193
+ Ping needs no added capability: it uses the unprivileged ICMP sockets that
194
+ Docker and containerd enable by default, so it also works with
195
+ `--cap-drop ALL` and under Kubernetes' restricted pod security profile. To
196
+ scan your LAN rather than the container's network, add `--network host`
197
+ (Linux).
198
+
199
+ For IPMG Web, use the [`compose.yaml`](https://github.com/sameeralam3127/ipmg/blob/main/compose.yaml)
200
+ in this repository. Inside a container IPMG Web must listen on `0.0.0.0` or
201
+ nothing outside the container could reach it, so the compose file starts it
202
+ with `web --host 0.0.0.0` and publishes the port on the host's `127.0.0.1`
203
+ only. Set a fixed `IPMG_WEB_TOKEN` in `.env`, then:
204
+
205
+ ```bash
206
+ docker compose up -d # http://127.0.0.1:8080/#token=<your token>
207
+ docker compose run --rm scan --input 10.0.0.0/24 # CLI scans land in the same history
208
+ ```
209
+
210
+ Publishing the port more widely (`8080:8080`) exposes IPMG Web on your
211
+ network over plain HTTP; put a reverse proxy with TLS in front first. Images
212
+ are tagged with the release version (`2.4.0`), the minor line (`2.4`), and
213
+ `latest`.
214
+
167
215
  ### Installing with pip
168
216
 
169
217
  `pip install ipmg` works inside a virtual environment, and inside one only.
@@ -193,6 +241,22 @@ python3 -m venv ~/.venvs/ipmg
193
241
 
194
242
  Please do not reach for `--break-system-packages`. It does what it says.
195
243
 
244
+ ### What gets installed
245
+
246
+ `pip install ipmg` is the lean core: scanning, every report format
247
+ (Excel included), history, change detection, notifications, and exit codes.
248
+ It is about 9 MB and 7 packages. IPMG Web, the browser UI with its REST API
249
+ and Prometheus `/metrics`, is the optional `web` extra, because its server
250
+ stack is most of the weight:
251
+
252
+ ```bash
253
+ pip install "ipmg[web]" # or: uv tool install "ipmg[web]", pipx install "ipmg[web]"
254
+ ```
255
+
256
+ The one-line installers, Homebrew, and the Docker image install it with the
257
+ `web` extra already. Run `ipmg web` without it and IPMG tells you the command
258
+ that adds it.
259
+
196
260
  ### The one thing IPMG needs from your system
197
261
 
198
262
  IPMG probes hosts with your operating system's `ping` command, and minimal
@@ -494,6 +558,57 @@ your own scripts. The [API guide](https://github.com/sameeralam3127/ipmg/blob/ma
494
558
  covers authentication, errors, and worked `curl` examples; the running server
495
559
  also serves interactive docs at `http://127.0.0.1:8080/docs`.
496
560
 
561
+ ### Prometheus metrics
562
+
563
+ `ipmg web --metrics` serves `/metrics` in the Prometheus text format, so the
564
+ results of every scan, whether run from cron with the CLI or from the browser,
565
+ land in the dashboards and alerts you already have. It reports the latest
566
+ completed scan of each source, and it is off unless you pass the flag:
567
+
568
+ ```bash
569
+ IPMG_WEB_TOKEN=$(cat /etc/ipmg/token) ipmg web --metrics --no-browser
570
+ ```
571
+
572
+ ```yaml
573
+ # prometheus.yml: the token is the same one the web UI uses
574
+ scrape_configs:
575
+ - job_name: ipmg
576
+ authorization:
577
+ credentials_file: /etc/prometheus/ipmg-token
578
+ static_configs:
579
+ - targets: ["127.0.0.1:8080"]
580
+
581
+ # an alert rule: a host that stopped answering
582
+ groups:
583
+ - name: ipmg
584
+ rules:
585
+ - alert: HostDown
586
+ expr: ipmg_host_up == 0
587
+ for: 10m
588
+ annotations:
589
+ summary: "{{ $labels.ip }} ({{ $labels.source }}) is not answering ping"
590
+ ```
591
+
592
+ | Metric | Labels | Meaning |
593
+ | --- | --- | --- |
594
+ | `ipmg_host_up` | `source`, `ip` | 1 if the host answered, else 0 |
595
+ | `ipmg_host_latency_seconds` | `source`, `ip` | Round-trip time, for hosts that answered |
596
+ | `ipmg_host_open_ports` | `source`, `ip` | Open TCP ports, for scans run with `--scan-ports` |
597
+ | `ipmg_host_info` | `source`, `ip`, `hostname` | Always 1; carries the reverse DNS name |
598
+ | `ipmg_scan_hosts` | `source`, `status` | Hosts in the latest scan, by status |
599
+ | `ipmg_scan_duration_seconds` | `source` | How long the latest scan took |
600
+ | `ipmg_scan_timestamp_seconds` | `source` | When the latest scan finished; alert on `time() - this` to catch a stalled cron job |
601
+ | `ipmg_scans_total` | `source` | Completed scans stored for the source |
602
+ | `ipmg_metrics_sources_omitted` | | Sources left out by `--metrics-sources` |
603
+ | `ipmg_build_info` | `version` | Always 1 |
604
+
605
+ **Cardinality is bounded.** Host series carry only `source` and `ip`, never
606
+ the hostname or status, which change, so a rename does not start a new series.
607
+ Only the `--metrics-sources` most recently scanned sources are exported
608
+ (default 20), and a scan holds at most 65,536 hosts, so the page stays at
609
+ about `sources × hosts × 4` series at most. Give recurring scans a stable
610
+ input (the same file) so each one updates its source rather than adding one.
611
+
497
612
  ### On a server with no browser
498
613
 
499
614
  On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
@@ -517,9 +632,11 @@ of it (see [Security](#security)).
517
632
 
518
633
  Anywhere IPMG takes `--input`, you can give it any of these:
519
634
 
520
- - **A single IP** — `8.8.8.8`
521
- - **A CIDR block** — `10.0.0.0/24`
522
- - **A range** — `10.0.0.1-10.0.0.200`
635
+ - **A single IP** — `8.8.8.8`, or IPv6 such as `2001:db8::1` or a link-local
636
+ `fe80::1%eth0`
637
+ - **A CIDR block** — `10.0.0.0/24`, or an IPv6 prefix up to 65,536 hosts
638
+ (`2001:db8::/112`)
639
+ - **A range** — `10.0.0.1-10.0.0.200` or `2001:db8::10-2001:db8::20`
523
640
  - **A text file** — one IP or CIDR per line. Blank lines and `#` comments are
524
641
  ignored, so you can annotate it:
525
642
 
@@ -529,7 +646,7 @@ Anywhere IPMG takes `--input`, you can give it any of these:
529
646
  192.168.1.0/30
530
647
  ```
531
648
 
532
- - **An Excel or CSV file** (`.xlsx`, `.xls`, `.csv`) — must contain a column
649
+ - **An Excel or CSV file** (`.xlsx`, `.csv`) — must contain a column
533
650
  named `IP Address`. Cells can hold single addresses or CIDR blocks:
534
651
 
535
652
  | IP Address |
@@ -559,6 +676,16 @@ ipmg --input targets.txt 10.0.0.0/30 10.0.0.5
559
676
  ipmg --input targets.txt --input 10.0.0.5 # the same thing
560
677
  ```
561
678
 
679
+ IPv4 and IPv6 targets mix freely in one scan, and reports, history, and change
680
+ detection handle both. An IPv6 `/64` holds 2^64 addresses, far too many to
681
+ sweep one by one, so it is rejected with a pointer to `--discover ipv6`, which
682
+ finds the hosts on your local links through neighbour discovery instead:
683
+
684
+ ```bash
685
+ ipmg --discover ipv6 # IPv6 neighbours on the local links
686
+ ipmg --discover all # the IPv4 /24 and the IPv6 neighbours together
687
+ ```
688
+
562
689
  Duplicate targets are removed automatically — across sources too, so a host
563
690
  that is both in the file and on the command line is scanned once — and one scan
564
691
  expands to at most 65,536 hosts in total. Larger CIDR blocks, ranges, or
@@ -708,7 +835,7 @@ cannot combine flags that exclude each other, such as `json` and `jsonl`.
708
835
  | Flag | Default | Description |
709
836
  | --- | --- | --- |
710
837
  | `--input` | `ip_list.xlsx` | What to scan: one or more files (`.xlsx`, `.xls`, `.csv`, `.json`, `.txt`, `.list`), IPs, CIDR blocks, or ranges (`10.0.0.1-10.0.0.50`), merged and de-duplicated |
711
- | `--discover` | off | Auto-detect and scan the local subnet instead |
838
+ | `--discover [FAMILY]` | off | Scan this machine's networks instead: `ipv4` (the default) sweeps the local /24, `ipv6` finds link neighbours, `all` does both |
712
839
  | `--output` | `results` | Report file name prefix |
713
840
  | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` (no file at all with `--json`/`--jsonl`) |
714
841
  | `--json` | off | Print the finished scan to stdout as a JSON array, human output on stderr |
@@ -1,52 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: ipmg
3
- Version: 2.4.0
4
- Summary: IP Management & Ping Monitoring CLI Tool
5
- Author: Sameer Alam
6
- Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
7
- License-Expression: MIT
8
- Project-URL: Homepage, https://sameeralam3127.github.io/ipmg/
9
- Project-URL: Documentation, https://github.com/sameeralam3127/ipmg#readme
10
- Project-URL: Repository, https://github.com/sameeralam3127/ipmg
11
- Project-URL: Issues, https://github.com/sameeralam3127/ipmg/issues
12
- Project-URL: Changelog, https://github.com/sameeralam3127/ipmg/releases
13
- Keywords: ip,network,ping,monitoring,cli,port-scan,network-scanner,ping-sweep,subnet-scanner,homelab,sysadmin
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3 :: Only
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Programming Language :: Python :: 3.14
22
- Classifier: Topic :: Internet
23
- Classifier: Topic :: System :: Networking
24
- Classifier: Topic :: System :: Monitoring
25
- Classifier: Intended Audience :: System Administrators
26
- Classifier: Intended Audience :: Information Technology
27
- Classifier: Operating System :: OS Independent
28
- Classifier: Environment :: Console
29
- Requires-Python: >=3.9
30
- Description-Content-Type: text/markdown
31
- License-File: LICENSE
32
- Requires-Dist: pandas>=2.2.2
33
- Requires-Dist: openpyxl>=3.1
34
- Requires-Dist: rich>=13.0
35
- Requires-Dist: fastapi>=0.110
36
- Requires-Dist: pydantic>=2.7
37
- Requires-Dist: uvicorn>=0.27
38
- Requires-Dist: websockets>=12
39
- Requires-Dist: python-multipart>=0.0.9
40
- Requires-Dist: tomli>=2.0.1; python_version < "3.11"
41
- Provides-Extra: dev
42
- Requires-Dist: pytest>=7.4; extra == "dev"
43
- Requires-Dist: pytest-cov>=5.0; extra == "dev"
44
- Requires-Dist: ruff>=0.4; extra == "dev"
45
- Requires-Dist: pre-commit>=3.5.0; extra == "dev"
46
- Requires-Dist: python-semantic-release>=10.5.3; extra == "dev"
47
- Requires-Dist: httpx>=0.27; extra == "dev"
48
- Dynamic: license-file
49
-
50
1
  # IPMG — IP Management & Ping Monitoring Tool
51
2
 
52
3
  [![PyPI](https://img.shields.io/pypi/v/ipmg)](https://pypi.org/project/ipmg/)
@@ -151,11 +102,24 @@ The formula lives in
151
102
  and is updated with every release. Upgrade with `brew upgrade ipmg`, remove
152
103
  with `brew uninstall ipmg`.
153
104
 
154
- **Windows — two commands in PowerShell:**
105
+ **Windows — one command in PowerShell:**
155
106
 
156
107
  ```powershell
157
- powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
158
- uv tool install ipmg
108
+ irm https://raw.githubusercontent.com/sameeralam3127/ipmg/main/install.ps1 | iex
109
+ ```
110
+
111
+ It works in Windows PowerShell 5.1 and PowerShell 7 and needs no
112
+ administrator rights: it installs uv if it is missing, installs IPMG, adds it
113
+ to your user `PATH`, and checks that `ipmg --version` runs. To pin a release:
114
+
115
+ ```powershell
116
+ & ([scriptblock]::Create((irm https://raw.githubusercontent.com/sameeralam3127/ipmg/main/install.ps1))) -Version 2.3.0
117
+ ```
118
+
119
+ **Docker (amd64 and arm64):**
120
+
121
+ ```bash
122
+ docker run --rm ghcr.io/sameeralam3127/ipmg --input 10.0.0.0/24
159
123
  ```
160
124
 
161
125
  Then check it works, on any platform:
@@ -164,6 +128,38 @@ Then check it works, on any platform:
164
128
  ipmg --version
165
129
  ```
166
130
 
131
+ ### Running in Docker
132
+
133
+ The image carries `ping`, runs as an unprivileged user (uid 10001), and keeps
134
+ everything a scan writes (reports and the history database) in `/data`. Mount
135
+ a volume there to keep it between runs:
136
+
137
+ ```bash
138
+ docker run --rm -v ipmg-data:/data ghcr.io/sameeralam3127/ipmg --input 10.0.0.0/24 --compare
139
+ ```
140
+
141
+ Ping needs no added capability: it uses the unprivileged ICMP sockets that
142
+ Docker and containerd enable by default, so it also works with
143
+ `--cap-drop ALL` and under Kubernetes' restricted pod security profile. To
144
+ scan your LAN rather than the container's network, add `--network host`
145
+ (Linux).
146
+
147
+ For IPMG Web, use the [`compose.yaml`](https://github.com/sameeralam3127/ipmg/blob/main/compose.yaml)
148
+ in this repository. Inside a container IPMG Web must listen on `0.0.0.0` or
149
+ nothing outside the container could reach it, so the compose file starts it
150
+ with `web --host 0.0.0.0` and publishes the port on the host's `127.0.0.1`
151
+ only. Set a fixed `IPMG_WEB_TOKEN` in `.env`, then:
152
+
153
+ ```bash
154
+ docker compose up -d # http://127.0.0.1:8080/#token=<your token>
155
+ docker compose run --rm scan --input 10.0.0.0/24 # CLI scans land in the same history
156
+ ```
157
+
158
+ Publishing the port more widely (`8080:8080`) exposes IPMG Web on your
159
+ network over plain HTTP; put a reverse proxy with TLS in front first. Images
160
+ are tagged with the release version (`2.4.0`), the minor line (`2.4`), and
161
+ `latest`.
162
+
167
163
  ### Installing with pip
168
164
 
169
165
  `pip install ipmg` works inside a virtual environment, and inside one only.
@@ -193,6 +189,22 @@ python3 -m venv ~/.venvs/ipmg
193
189
 
194
190
  Please do not reach for `--break-system-packages`. It does what it says.
195
191
 
192
+ ### What gets installed
193
+
194
+ `pip install ipmg` is the lean core: scanning, every report format
195
+ (Excel included), history, change detection, notifications, and exit codes.
196
+ It is about 9 MB and 7 packages. IPMG Web, the browser UI with its REST API
197
+ and Prometheus `/metrics`, is the optional `web` extra, because its server
198
+ stack is most of the weight:
199
+
200
+ ```bash
201
+ pip install "ipmg[web]" # or: uv tool install "ipmg[web]", pipx install "ipmg[web]"
202
+ ```
203
+
204
+ The one-line installers, Homebrew, and the Docker image install it with the
205
+ `web` extra already. Run `ipmg web` without it and IPMG tells you the command
206
+ that adds it.
207
+
196
208
  ### The one thing IPMG needs from your system
197
209
 
198
210
  IPMG probes hosts with your operating system's `ping` command, and minimal
@@ -494,6 +506,57 @@ your own scripts. The [API guide](https://github.com/sameeralam3127/ipmg/blob/ma
494
506
  covers authentication, errors, and worked `curl` examples; the running server
495
507
  also serves interactive docs at `http://127.0.0.1:8080/docs`.
496
508
 
509
+ ### Prometheus metrics
510
+
511
+ `ipmg web --metrics` serves `/metrics` in the Prometheus text format, so the
512
+ results of every scan, whether run from cron with the CLI or from the browser,
513
+ land in the dashboards and alerts you already have. It reports the latest
514
+ completed scan of each source, and it is off unless you pass the flag:
515
+
516
+ ```bash
517
+ IPMG_WEB_TOKEN=$(cat /etc/ipmg/token) ipmg web --metrics --no-browser
518
+ ```
519
+
520
+ ```yaml
521
+ # prometheus.yml: the token is the same one the web UI uses
522
+ scrape_configs:
523
+ - job_name: ipmg
524
+ authorization:
525
+ credentials_file: /etc/prometheus/ipmg-token
526
+ static_configs:
527
+ - targets: ["127.0.0.1:8080"]
528
+
529
+ # an alert rule: a host that stopped answering
530
+ groups:
531
+ - name: ipmg
532
+ rules:
533
+ - alert: HostDown
534
+ expr: ipmg_host_up == 0
535
+ for: 10m
536
+ annotations:
537
+ summary: "{{ $labels.ip }} ({{ $labels.source }}) is not answering ping"
538
+ ```
539
+
540
+ | Metric | Labels | Meaning |
541
+ | --- | --- | --- |
542
+ | `ipmg_host_up` | `source`, `ip` | 1 if the host answered, else 0 |
543
+ | `ipmg_host_latency_seconds` | `source`, `ip` | Round-trip time, for hosts that answered |
544
+ | `ipmg_host_open_ports` | `source`, `ip` | Open TCP ports, for scans run with `--scan-ports` |
545
+ | `ipmg_host_info` | `source`, `ip`, `hostname` | Always 1; carries the reverse DNS name |
546
+ | `ipmg_scan_hosts` | `source`, `status` | Hosts in the latest scan, by status |
547
+ | `ipmg_scan_duration_seconds` | `source` | How long the latest scan took |
548
+ | `ipmg_scan_timestamp_seconds` | `source` | When the latest scan finished; alert on `time() - this` to catch a stalled cron job |
549
+ | `ipmg_scans_total` | `source` | Completed scans stored for the source |
550
+ | `ipmg_metrics_sources_omitted` | | Sources left out by `--metrics-sources` |
551
+ | `ipmg_build_info` | `version` | Always 1 |
552
+
553
+ **Cardinality is bounded.** Host series carry only `source` and `ip`, never
554
+ the hostname or status, which change, so a rename does not start a new series.
555
+ Only the `--metrics-sources` most recently scanned sources are exported
556
+ (default 20), and a scan holds at most 65,536 hosts, so the page stays at
557
+ about `sources × hosts × 4` series at most. Give recurring scans a stable
558
+ input (the same file) so each one updates its source rather than adding one.
559
+
497
560
  ### On a server with no browser
498
561
 
499
562
  On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
@@ -517,9 +580,11 @@ of it (see [Security](#security)).
517
580
 
518
581
  Anywhere IPMG takes `--input`, you can give it any of these:
519
582
 
520
- - **A single IP** — `8.8.8.8`
521
- - **A CIDR block** — `10.0.0.0/24`
522
- - **A range** — `10.0.0.1-10.0.0.200`
583
+ - **A single IP** — `8.8.8.8`, or IPv6 such as `2001:db8::1` or a link-local
584
+ `fe80::1%eth0`
585
+ - **A CIDR block** — `10.0.0.0/24`, or an IPv6 prefix up to 65,536 hosts
586
+ (`2001:db8::/112`)
587
+ - **A range** — `10.0.0.1-10.0.0.200` or `2001:db8::10-2001:db8::20`
523
588
  - **A text file** — one IP or CIDR per line. Blank lines and `#` comments are
524
589
  ignored, so you can annotate it:
525
590
 
@@ -529,7 +594,7 @@ Anywhere IPMG takes `--input`, you can give it any of these:
529
594
  192.168.1.0/30
530
595
  ```
531
596
 
532
- - **An Excel or CSV file** (`.xlsx`, `.xls`, `.csv`) — must contain a column
597
+ - **An Excel or CSV file** (`.xlsx`, `.csv`) — must contain a column
533
598
  named `IP Address`. Cells can hold single addresses or CIDR blocks:
534
599
 
535
600
  | IP Address |
@@ -559,6 +624,16 @@ ipmg --input targets.txt 10.0.0.0/30 10.0.0.5
559
624
  ipmg --input targets.txt --input 10.0.0.5 # the same thing
560
625
  ```
561
626
 
627
+ IPv4 and IPv6 targets mix freely in one scan, and reports, history, and change
628
+ detection handle both. An IPv6 `/64` holds 2^64 addresses, far too many to
629
+ sweep one by one, so it is rejected with a pointer to `--discover ipv6`, which
630
+ finds the hosts on your local links through neighbour discovery instead:
631
+
632
+ ```bash
633
+ ipmg --discover ipv6 # IPv6 neighbours on the local links
634
+ ipmg --discover all # the IPv4 /24 and the IPv6 neighbours together
635
+ ```
636
+
562
637
  Duplicate targets are removed automatically — across sources too, so a host
563
638
  that is both in the file and on the command line is scanned once — and one scan
564
639
  expands to at most 65,536 hosts in total. Larger CIDR blocks, ranges, or
@@ -708,7 +783,7 @@ cannot combine flags that exclude each other, such as `json` and `jsonl`.
708
783
  | Flag | Default | Description |
709
784
  | --- | --- | --- |
710
785
  | `--input` | `ip_list.xlsx` | What to scan: one or more files (`.xlsx`, `.xls`, `.csv`, `.json`, `.txt`, `.list`), IPs, CIDR blocks, or ranges (`10.0.0.1-10.0.0.50`), merged and de-duplicated |
711
- | `--discover` | off | Auto-detect and scan the local subnet instead |
786
+ | `--discover [FAMILY]` | off | Scan this machine's networks instead: `ipv4` (the default) sweeps the local /24, `ipv6` finds link neighbours, `all` does both |
712
787
  | `--output` | `results` | Report file name prefix |
713
788
  | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` (no file at all with `--json`/`--jsonl`) |
714
789
  | `--json` | off | Print the finished scan to stdout as a JSON array, human output on stderr |
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
13
13
 
14
14
  [project]
15
15
  name = "ipmg"
16
- version = "2.4.0" # Managed automatically by semantic-release
16
+ version = "3.1.0" # Managed automatically by semantic-release
17
17
  description = "IP Management & Ping Monitoring CLI Tool"
18
18
  readme = "README.md"
19
19
  requires-python = ">=3.9"
@@ -48,15 +48,11 @@ classifiers = [
48
48
  "Environment :: Console",
49
49
  ]
50
50
 
51
+ # The core is what a scan needs: the terminal UI, Excel reports and input,
52
+ # and the config file parser. IPMG Web's server stack is the `web` extra.
51
53
  dependencies = [
52
- "pandas>=2.2.2",
53
54
  "openpyxl>=3.1",
54
55
  "rich>=13.0",
55
- "fastapi>=0.110",
56
- "pydantic>=2.7",
57
- "uvicorn>=0.27",
58
- "websockets>=12",
59
- "python-multipart>=0.0.9",
60
56
  # tomllib is in the standard library from 3.11; tomli 2.x is the same
61
57
  # parser with the same API, so the floor must not drop below it.
62
58
  "tomli>=2.0.1; python_version < '3.11'",
@@ -70,7 +66,18 @@ Issues = "https://github.com/sameeralam3127/ipmg/issues"
70
66
  Changelog = "https://github.com/sameeralam3127/ipmg/releases"
71
67
 
72
68
  [project.optional-dependencies]
69
+ # IPMG Web (`ipmg web`): the browser UI, its REST API, and /metrics.
70
+ web = [
71
+ "fastapi>=0.110",
72
+ "pydantic>=2.7",
73
+ "uvicorn>=0.27",
74
+ "websockets>=12",
75
+ "python-multipart>=0.0.9",
76
+ ]
77
+ # Everything; what the one-line installers and the Docker image install.
78
+ all = ["ipmg[web]"]
73
79
  dev = [
80
+ "ipmg[web]",
74
81
  "pytest>=7.4",
75
82
  "pytest-cov>=5.0",
76
83
  "ruff>=0.4",
@@ -89,6 +96,7 @@ ipmg = "ipmg.cli.commands:run"
89
96
 
90
97
  [dependency-groups]
91
98
  dev = [
99
+ "ipmg[web]",
92
100
  "pytest>=7.4",
93
101
  "pytest-cov>=5.0",
94
102
  "ruff>=0.4",
@@ -2,4 +2,4 @@
2
2
  ipmg - IP Management & Ping Monitoring Tool
3
3
  """
4
4
 
5
- __version__ = "2.4.0"
5
+ __version__ = "3.1.0"
@@ -25,6 +25,17 @@ from ipmg.services.history_service import HistoryService
25
25
  from ipmg.services.scan_service import machine_output, run_scan
26
26
  from ipmg.utils.helpers import configure_logging
27
27
 
28
+ #: Top-level modules of the `web` extra, to tell a missing extra from a bug.
29
+ WEB_PACKAGES = frozenset({"fastapi", "pydantic", "starlette", "uvicorn", "websockets", "multipart"})
30
+
31
+ WEB_EXTRA_MISSING = (
32
+ "IPMG Web needs the optional 'web' packages, which this install does not have. "
33
+ "Add them with whichever tool installed IPMG:\n"
34
+ " uv tool install --force 'ipmg[web]'\n"
35
+ " pipx install --force 'ipmg[web]'\n"
36
+ " pip install 'ipmg[web]'"
37
+ )
38
+
28
39
  EXIT_OK = 0
29
40
  EXIT_ERROR = 1
30
41
  EXIT_CHANGES_DETECTED = 2
@@ -57,14 +68,21 @@ def _web_command(argv: List[str]) -> int:
57
68
  ui.header("web")
58
69
  print_disclaimer_once()
59
70
 
60
- # Imported lazily so plain CLI scans do not pay for the web stack.
61
- from ipmg.web.server import run_dashboard
71
+ # Imported lazily so plain CLI scans do not pay for the web stack, which
72
+ # is the optional `web` extra.
73
+ try:
74
+ from ipmg.web.server import run_dashboard
75
+ except ModuleNotFoundError as exc:
76
+ if exc.name not in WEB_PACKAGES:
77
+ raise
78
+ raise IPMGError(WEB_EXTRA_MISSING) from exc
62
79
 
63
80
  run_dashboard(
64
81
  host=args.host,
65
82
  port=args.port,
66
83
  open_browser=not args.no_browser,
67
84
  db_path=args.db,
85
+ metrics_sources=max(args.metrics_sources, 1) if args.metrics else None,
68
86
  )
69
87
  return EXIT_OK
70
88
 
@@ -264,6 +264,11 @@ def _coerce(action: argparse.Action, key: str, value: Any, path: Path) -> Any:
264
264
  raise ConfigError(f"{where} must be true or false, not {_kind(value)}.")
265
265
  return action.const if value else _KEEP
266
266
 
267
+ if action.nargs == "?" and isinstance(value, bool):
268
+ # A flag whose value is optional (--discover [FAMILY]): true is the bare
269
+ # flag, so "discover = true" keeps meaning what it did as a switch.
270
+ return action.const if value else _KEEP
271
+
267
272
  if action.nargs in ("+", "*"):
268
273
  values = value if isinstance(value, list) else [value]
269
274
  if not values:
@@ -6,6 +6,7 @@ import argparse
6
6
 
7
7
  from ipmg import __version__
8
8
  from ipmg.cli.config import add_config_arguments, config_help
9
+ from ipmg.core.discovery import DISCOVERY_FAMILIES
9
10
  from ipmg.core.portscan import DEFAULT_PORTS, parse_port_list
10
11
  from ipmg.infrastructure.file_io import DEFAULT_INPUT_FILE
11
12
  from ipmg.infrastructure.incremental import (
@@ -26,6 +27,7 @@ from ipmg.infrastructure.notify import (
26
27
  )
27
28
  from ipmg.reporting.diff_report import DIFF_FORMATS
28
29
  from ipmg.reporting.live import DEFAULT_REFRESH_S, MAX_REFRESH_S, MIN_REFRESH_S
30
+ from ipmg.reporting.metrics import DEFAULT_MAX_SOURCES
29
31
 
30
32
  PROG = "IPMG - IP Management & Ping Monitoring Tool"
31
33
 
@@ -259,8 +261,16 @@ def build_parser() -> argparse.ArgumentParser:
259
261
  )
260
262
  parser.add_argument(
261
263
  "--discover",
262
- action="store_true",
263
- help="Auto-detect this machine's subnet and scan it instead of --input.",
264
+ nargs="?",
265
+ const="ipv4",
266
+ default=None,
267
+ choices=DISCOVERY_FAMILIES,
268
+ metavar="FAMILY",
269
+ help=(
270
+ "Scan this machine's networks instead of --input: ipv4 (the default) "
271
+ "sweeps the local /24, ipv6 finds hosts on the local links through "
272
+ "neighbour discovery, all does both."
273
+ ),
264
274
  )
265
275
  parser.add_argument(
266
276
  "--resolve",
@@ -449,6 +459,24 @@ def build_web_parser() -> argparse.ArgumentParser:
449
459
  action="store_true",
450
460
  help="Do not open IPMG Web in a browser automatically.",
451
461
  )
462
+ parser.add_argument(
463
+ "--metrics",
464
+ action="store_true",
465
+ help=(
466
+ "Serve Prometheus metrics at /metrics for the latest completed scan of "
467
+ "each source (off by default; needs the access token)."
468
+ ),
469
+ )
470
+ parser.add_argument(
471
+ "--metrics-sources",
472
+ type=int,
473
+ default=DEFAULT_MAX_SOURCES,
474
+ metavar="N",
475
+ help=(
476
+ "Export only the N most recently scanned sources, which bounds the "
477
+ f"number of series (default: {DEFAULT_MAX_SOURCES})."
478
+ ),
479
+ )
452
480
  _add_database_argument(parser)
453
481
  parser.add_argument("--verbose", action="store_true")
454
482
  return parser
@@ -256,13 +256,17 @@ def _index_by_hostname(snapshots: Mapping[str, HostSnapshot]) -> Dict[str, Set[s
256
256
  return index
257
257
 
258
258
 
259
- def ip_sort_key(value: str) -> Tuple[int, int, str]:
260
- """Sort key placing valid IPs first in numeric order; shared with the DB layer."""
259
+ def ip_sort_key(value: str) -> Tuple[int, int, int, str]:
260
+ """Sort key: IPv4 in numeric order, then IPv6, then anything unparsable.
261
+
262
+ The family comes before the number, or ``::1`` (the integer 1) would sort
263
+ ahead of every IPv4 address. Shared with the DB layer.
264
+ """
261
265
  try:
262
266
  address = ipaddress.ip_address(value)
263
267
  except ValueError:
264
- return (1, 0, value)
265
- return (0, int(address), "")
268
+ return (1, 0, 0, value)
269
+ return (0, address.version, int(address), value)
266
270
 
267
271
 
268
272
  def _sort_changes(changes: Sequence[HostChange]) -> Tuple[HostChange, ...]: