ipmg 2.0.0__tar.gz → 2.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.
- {ipmg-2.0.0/src/ipmg.egg-info → ipmg-2.1.0}/PKG-INFO +63 -6
- {ipmg-2.0.0 → ipmg-2.1.0}/README.md +59 -3
- {ipmg-2.0.0 → ipmg-2.1.0}/pyproject.toml +13 -3
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/__init__.py +1 -1
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/cli/parser.py +26 -2
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/core/engine.py +6 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/infrastructure/file_io.py +45 -15
- ipmg-2.1.0/src/ipmg/infrastructure/incremental.py +276 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/services/scan_service.py +89 -6
- {ipmg-2.0.0 → ipmg-2.1.0/src/ipmg.egg-info}/PKG-INFO +63 -6
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg.egg-info/SOURCES.txt +2 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg.egg-info/requires.txt +2 -1
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_engine.py +1 -3
- ipmg-2.1.0/tests/test_incremental.py +227 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_scan_service.py +11 -11
- {ipmg-2.0.0 → ipmg-2.1.0}/LICENSE +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/setup.cfg +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/__main__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/cli/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/cli/commands.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/core/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/core/diff.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/core/discovery.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/core/ping.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/core/portscan.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/core/security.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/exceptions.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/infrastructure/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/infrastructure/database.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/reporting/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/reporting/diff_report.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/reporting/frames.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/reporting/live.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/reporting/summary.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/reporting/ui.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/services/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/services/history_service.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/utils/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/utils/helpers.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/__init__.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/app.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/db.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/manager.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/server.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/static/css/app.css +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/static/index.html +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/static/js/api.js +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/static/js/app.js +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/static/js/charts.js +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/static/js/demo.js +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg/web/static/js/views.js +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg.egg-info/dependency_links.txt +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg.egg-info/entry_points.txt +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/src/ipmg.egg-info/top_level.txt +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_commands.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_database_history.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_diff.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_diff_report.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_discover.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_file_io.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_history_service.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_live.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_parser.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_ping.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_ping_command.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_portscan.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_ui.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_utils.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_web_api.py +0 -0
- {ipmg-2.0.0 → ipmg-2.1.0}/tests/test_web_db.py +0 -0
- {ipmg-2.0.0 → ipmg-2.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.
|
|
3
|
+
Version: 2.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>
|
|
@@ -10,7 +10,7 @@ Project-URL: Documentation, https://github.com/sameeralam3127/ipmg#readme
|
|
|
10
10
|
Project-URL: Repository, https://github.com/sameeralam3127/ipmg
|
|
11
11
|
Project-URL: Issues, https://github.com/sameeralam3127/ipmg/issues
|
|
12
12
|
Project-URL: Changelog, https://github.com/sameeralam3127/ipmg/releases
|
|
13
|
-
Keywords: ip,network,ping,monitoring,cli,port-scan
|
|
13
|
+
Keywords: ip,network,ping,monitoring,cli,port-scan,network-scanner,ping-sweep,subnet-scanner,homelab,sysadmin
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -29,7 +29,7 @@ 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.
|
|
32
|
+
Requires-Dist: pandas>=2.2.2
|
|
33
33
|
Requires-Dist: openpyxl>=3.1
|
|
34
34
|
Requires-Dist: rich>=13.0
|
|
35
35
|
Requires-Dist: fastapi>=0.110
|
|
@@ -38,6 +38,7 @@ Requires-Dist: websockets>=12
|
|
|
38
38
|
Requires-Dist: python-multipart>=0.0.9
|
|
39
39
|
Provides-Extra: dev
|
|
40
40
|
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
41
42
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
42
43
|
Requires-Dist: pre-commit>=3.5.0; extra == "dev"
|
|
43
44
|
Requires-Dist: python-semantic-release>=10.5.3; extra == "dev"
|
|
@@ -58,8 +59,19 @@ can send to someone: Excel, CSV, JSON, or Markdown. It works from the command
|
|
|
58
59
|
line or from IPMG Web, a local browser UI, and it remembers every scan so it can tell
|
|
59
60
|
you what moved.
|
|
60
61
|
|
|
62
|
+
<p align="center">
|
|
63
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-demo.gif" alt="ipmg scanning 13 hosts in parallel: live results with reverse DNS names and latency, then a summary of 10 active and 3 timed out" width="820">
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
**Why not `nmap -sn`, `fping`, or Angry IP Scanner?** They tell you what is up
|
|
67
|
+
right now. IPMG also remembers every scan and tells you what changed since the
|
|
68
|
+
last one: the host that dropped off, the device that appeared, the latency that
|
|
69
|
+
doubled. It writes the report you would otherwise build by hand.
|
|
70
|
+
[How it compares](#how-it-compares)
|
|
71
|
+
|
|
61
72
|
**Website:** [sameeralam3127.github.io/ipmg](https://sameeralam3127.github.io/ipmg/) ·
|
|
62
|
-
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/)
|
|
73
|
+
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/) ·
|
|
74
|
+
**Command builder:** [pick what you want, copy the command](https://sameeralam3127.github.io/ipmg/#builder)
|
|
63
75
|
|
|
64
76
|
```bash
|
|
65
77
|
pip install ipmg
|
|
@@ -71,6 +83,7 @@ ipmg --discover # scan the network you are on, right now
|
|
|
71
83
|
|
|
72
84
|
**Contents**
|
|
73
85
|
|
|
86
|
+
[How it compares](#how-it-compares) ·
|
|
74
87
|
[Install](#install) ·
|
|
75
88
|
[Your first scan](#your-first-scan) ·
|
|
76
89
|
[Common tasks](#common-tasks) ·
|
|
@@ -86,6 +99,26 @@ ipmg --discover # scan the network you are on, right now
|
|
|
86
99
|
|
|
87
100
|
---
|
|
88
101
|
|
|
102
|
+
## How it compares
|
|
103
|
+
|
|
104
|
+
| | IPMG | `nmap -sn` | `fping` | Angry IP Scanner |
|
|
105
|
+
| --- | --- | --- | --- | --- |
|
|
106
|
+
| Parallel ping sweep | Yes | Yes | Yes | Yes |
|
|
107
|
+
| Scan history and "what changed" | Built in | Save XML, compare with `ndiff` | No | No |
|
|
108
|
+
| Reports | Excel, CSV, JSON, Markdown | XML, grepable text | Plain text | CSV, TXT, XML |
|
|
109
|
+
| Browser UI | IPMG Web, local | No (Zenmap is a desktop app) | No | Desktop app (Java) |
|
|
110
|
+
| Port checks | Common TCP ports | Full port and OS scanner | No | Via fetchers |
|
|
111
|
+
|
|
112
|
+
Reach for nmap when you need a real port or OS scanner. Reach for IPMG when you
|
|
113
|
+
look after a network and need to know what moved since yesterday, with a report
|
|
114
|
+
you can hand to someone.
|
|
115
|
+
|
|
116
|
+
<p align="center">
|
|
117
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-web.png" alt="IPMG Web dashboard: scan totals, a status donut of 16 active, 1 timeout and 1 inactive host, a latency trend chart, and a list of recent scans" width="820">
|
|
118
|
+
</p>
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
89
122
|
## Install
|
|
90
123
|
|
|
91
124
|
**Linux and macOS — one command, works on every distribution:**
|
|
@@ -280,6 +313,19 @@ exist yet. A file you name with `--input` must already exist.
|
|
|
280
313
|
|
|
281
314
|
## Common tasks
|
|
282
315
|
|
|
316
|
+
Not sure which flags you need? The
|
|
317
|
+
[command builder](https://sameeralam3127.github.io/ipmg/#builder) on the
|
|
318
|
+
website puts the command together as you pick what you want to know, and
|
|
319
|
+
explains every flag it adds.
|
|
320
|
+
|
|
321
|
+
<p align="center">
|
|
322
|
+
<a href="https://sameeralam3127.github.io/ipmg/#builder">
|
|
323
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-builder.png" alt="IPMG command builder: choose Scan, Compare, History, or Web, enter a target such as 192.168.1.0/24, switch on options like live results or hostnames, and copy the generated ipmg command with each flag explained" width="820">
|
|
324
|
+
</a>
|
|
325
|
+
</p>
|
|
326
|
+
|
|
327
|
+
Or pick a ready-made command:
|
|
328
|
+
|
|
283
329
|
| I want to… | Command |
|
|
284
330
|
| --- | --- |
|
|
285
331
|
| Scan the network I am on | `ipmg --discover` |
|
|
@@ -478,7 +524,16 @@ Status is one of `Active`, `Inactive`, `Timeout`, `Unreachable`, `Invalid IP`,
|
|
|
478
524
|
or `Error`. `Hostname` is filled in when you pass `--resolve`.
|
|
479
525
|
|
|
480
526
|
The `md` format produces a shareable Markdown report with a status summary
|
|
481
|
-
table — handy for tickets, handoffs, and incident timelines.
|
|
527
|
+
table — handy for tickets, handoffs, and incident timelines. `jsonl` writes one
|
|
528
|
+
JSON object per line, which streams into `jq` and log pipelines.
|
|
529
|
+
|
|
530
|
+
**Reports survive an interrupted scan.** A scan writes its report as it goes,
|
|
531
|
+
so pressing Ctrl+C halfway through a /16 leaves a valid report of everything
|
|
532
|
+
scanned so far instead of nothing at all. `csv` and `jsonl` are appended per
|
|
533
|
+
host; `xlsx`, `json`, and `md` are re-saved every `--autosave` seconds (30 by
|
|
534
|
+
default). A finished scan overwrites those files with the complete report, so
|
|
535
|
+
the file names and contents are the same as they always were. Use
|
|
536
|
+
`--no-incremental` to go back to writing only at the end.
|
|
482
537
|
|
|
483
538
|
**Open ports.** `Open Ports` is only populated when `--scan-ports` is set: for
|
|
484
539
|
each host that answers, IPMG probes a list of common TCP ports (SSH, HTTP,
|
|
@@ -507,7 +562,9 @@ them — so piping IPMG into a file or a log gives you clean text.
|
|
|
507
562
|
| `--input` | `ip_list.xlsx` | What to scan: a file (`.xlsx`, `.xls`, `.csv`, `.txt`, `.list`), a single IP, a CIDR block, or a range (`10.0.0.1-10.0.0.50`) |
|
|
508
563
|
| `--discover` | off | Auto-detect and scan the local subnet instead |
|
|
509
564
|
| `--output` | `results` | Report file name prefix |
|
|
510
|
-
| `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `md` |
|
|
565
|
+
| `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` |
|
|
566
|
+
| `--no-incremental` | off | Only write the report once the scan has finished |
|
|
567
|
+
| `--autosave` | `30` | How often a running scan re-saves `xlsx`, `json`, and `md` |
|
|
511
568
|
|
|
512
569
|
**Speed and accuracy**
|
|
513
570
|
|
|
@@ -12,8 +12,19 @@ can send to someone: Excel, CSV, JSON, or Markdown. It works from the command
|
|
|
12
12
|
line or from IPMG Web, a local browser UI, and it remembers every scan so it can tell
|
|
13
13
|
you what moved.
|
|
14
14
|
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-demo.gif" alt="ipmg scanning 13 hosts in parallel: live results with reverse DNS names and latency, then a summary of 10 active and 3 timed out" width="820">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
**Why not `nmap -sn`, `fping`, or Angry IP Scanner?** They tell you what is up
|
|
20
|
+
right now. IPMG also remembers every scan and tells you what changed since the
|
|
21
|
+
last one: the host that dropped off, the device that appeared, the latency that
|
|
22
|
+
doubled. It writes the report you would otherwise build by hand.
|
|
23
|
+
[How it compares](#how-it-compares)
|
|
24
|
+
|
|
15
25
|
**Website:** [sameeralam3127.github.io/ipmg](https://sameeralam3127.github.io/ipmg/) ·
|
|
16
|
-
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/)
|
|
26
|
+
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/) ·
|
|
27
|
+
**Command builder:** [pick what you want, copy the command](https://sameeralam3127.github.io/ipmg/#builder)
|
|
17
28
|
|
|
18
29
|
```bash
|
|
19
30
|
pip install ipmg
|
|
@@ -25,6 +36,7 @@ ipmg --discover # scan the network you are on, right now
|
|
|
25
36
|
|
|
26
37
|
**Contents**
|
|
27
38
|
|
|
39
|
+
[How it compares](#how-it-compares) ·
|
|
28
40
|
[Install](#install) ·
|
|
29
41
|
[Your first scan](#your-first-scan) ·
|
|
30
42
|
[Common tasks](#common-tasks) ·
|
|
@@ -40,6 +52,26 @@ ipmg --discover # scan the network you are on, right now
|
|
|
40
52
|
|
|
41
53
|
---
|
|
42
54
|
|
|
55
|
+
## How it compares
|
|
56
|
+
|
|
57
|
+
| | IPMG | `nmap -sn` | `fping` | Angry IP Scanner |
|
|
58
|
+
| --- | --- | --- | --- | --- |
|
|
59
|
+
| Parallel ping sweep | Yes | Yes | Yes | Yes |
|
|
60
|
+
| Scan history and "what changed" | Built in | Save XML, compare with `ndiff` | No | No |
|
|
61
|
+
| Reports | Excel, CSV, JSON, Markdown | XML, grepable text | Plain text | CSV, TXT, XML |
|
|
62
|
+
| Browser UI | IPMG Web, local | No (Zenmap is a desktop app) | No | Desktop app (Java) |
|
|
63
|
+
| Port checks | Common TCP ports | Full port and OS scanner | No | Via fetchers |
|
|
64
|
+
|
|
65
|
+
Reach for nmap when you need a real port or OS scanner. Reach for IPMG when you
|
|
66
|
+
look after a network and need to know what moved since yesterday, with a report
|
|
67
|
+
you can hand to someone.
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-web.png" alt="IPMG Web dashboard: scan totals, a status donut of 16 active, 1 timeout and 1 inactive host, a latency trend chart, and a list of recent scans" width="820">
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
43
75
|
## Install
|
|
44
76
|
|
|
45
77
|
**Linux and macOS — one command, works on every distribution:**
|
|
@@ -234,6 +266,19 @@ exist yet. A file you name with `--input` must already exist.
|
|
|
234
266
|
|
|
235
267
|
## Common tasks
|
|
236
268
|
|
|
269
|
+
Not sure which flags you need? The
|
|
270
|
+
[command builder](https://sameeralam3127.github.io/ipmg/#builder) on the
|
|
271
|
+
website puts the command together as you pick what you want to know, and
|
|
272
|
+
explains every flag it adds.
|
|
273
|
+
|
|
274
|
+
<p align="center">
|
|
275
|
+
<a href="https://sameeralam3127.github.io/ipmg/#builder">
|
|
276
|
+
<img src="https://raw.githubusercontent.com/sameeralam3127/ipmg/main/docs/assets/ipmg-builder.png" alt="IPMG command builder: choose Scan, Compare, History, or Web, enter a target such as 192.168.1.0/24, switch on options like live results or hostnames, and copy the generated ipmg command with each flag explained" width="820">
|
|
277
|
+
</a>
|
|
278
|
+
</p>
|
|
279
|
+
|
|
280
|
+
Or pick a ready-made command:
|
|
281
|
+
|
|
237
282
|
| I want to… | Command |
|
|
238
283
|
| --- | --- |
|
|
239
284
|
| Scan the network I am on | `ipmg --discover` |
|
|
@@ -432,7 +477,16 @@ Status is one of `Active`, `Inactive`, `Timeout`, `Unreachable`, `Invalid IP`,
|
|
|
432
477
|
or `Error`. `Hostname` is filled in when you pass `--resolve`.
|
|
433
478
|
|
|
434
479
|
The `md` format produces a shareable Markdown report with a status summary
|
|
435
|
-
table — handy for tickets, handoffs, and incident timelines.
|
|
480
|
+
table — handy for tickets, handoffs, and incident timelines. `jsonl` writes one
|
|
481
|
+
JSON object per line, which streams into `jq` and log pipelines.
|
|
482
|
+
|
|
483
|
+
**Reports survive an interrupted scan.** A scan writes its report as it goes,
|
|
484
|
+
so pressing Ctrl+C halfway through a /16 leaves a valid report of everything
|
|
485
|
+
scanned so far instead of nothing at all. `csv` and `jsonl` are appended per
|
|
486
|
+
host; `xlsx`, `json`, and `md` are re-saved every `--autosave` seconds (30 by
|
|
487
|
+
default). A finished scan overwrites those files with the complete report, so
|
|
488
|
+
the file names and contents are the same as they always were. Use
|
|
489
|
+
`--no-incremental` to go back to writing only at the end.
|
|
436
490
|
|
|
437
491
|
**Open ports.** `Open Ports` is only populated when `--scan-ports` is set: for
|
|
438
492
|
each host that answers, IPMG probes a list of common TCP ports (SSH, HTTP,
|
|
@@ -461,7 +515,9 @@ them — so piping IPMG into a file or a log gives you clean text.
|
|
|
461
515
|
| `--input` | `ip_list.xlsx` | What to scan: a file (`.xlsx`, `.xls`, `.csv`, `.txt`, `.list`), a single IP, a CIDR block, or a range (`10.0.0.1-10.0.0.50`) |
|
|
462
516
|
| `--discover` | off | Auto-detect and scan the local subnet instead |
|
|
463
517
|
| `--output` | `results` | Report file name prefix |
|
|
464
|
-
| `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `md` |
|
|
518
|
+
| `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` |
|
|
519
|
+
| `--no-incremental` | off | Only write the report once the scan has finished |
|
|
520
|
+
| `--autosave` | `30` | How often a running scan re-saves `xlsx`, `json`, and `md` |
|
|
465
521
|
|
|
466
522
|
**Speed and accuracy**
|
|
467
523
|
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "ipmg"
|
|
16
|
-
version = "2.
|
|
16
|
+
version = "2.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"
|
|
@@ -28,7 +28,7 @@ maintainers = [
|
|
|
28
28
|
{ name = "Sameer Alam", email = "sameeralam3127@gmail.com" }
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
-
keywords = ["ip", "network", "ping", "monitoring", "cli", "port-scan"]
|
|
31
|
+
keywords = ["ip", "network", "ping", "monitoring", "cli", "port-scan", "network-scanner", "ping-sweep", "subnet-scanner", "homelab", "sysadmin"]
|
|
32
32
|
|
|
33
33
|
classifiers = [
|
|
34
34
|
"Programming Language :: Python :: 3",
|
|
@@ -49,7 +49,7 @@ classifiers = [
|
|
|
49
49
|
]
|
|
50
50
|
|
|
51
51
|
dependencies = [
|
|
52
|
-
"pandas>=2.
|
|
52
|
+
"pandas>=2.2.2",
|
|
53
53
|
"openpyxl>=3.1",
|
|
54
54
|
"rich>=13.0",
|
|
55
55
|
"fastapi>=0.110",
|
|
@@ -68,6 +68,7 @@ Changelog = "https://github.com/sameeralam3127/ipmg/releases"
|
|
|
68
68
|
[project.optional-dependencies]
|
|
69
69
|
dev = [
|
|
70
70
|
"pytest>=7.4",
|
|
71
|
+
"pytest-cov>=5.0",
|
|
71
72
|
"ruff>=0.4",
|
|
72
73
|
"pre-commit>=3.5.0",
|
|
73
74
|
"python-semantic-release>=10.5.3",
|
|
@@ -85,6 +86,7 @@ ipmg = "ipmg.cli.commands:run"
|
|
|
85
86
|
[dependency-groups]
|
|
86
87
|
dev = [
|
|
87
88
|
"pytest>=7.4",
|
|
89
|
+
"pytest-cov>=5.0",
|
|
88
90
|
"ruff>=0.4",
|
|
89
91
|
"pre-commit>=3.5.0",
|
|
90
92
|
"python-semantic-release>=10.5.3",
|
|
@@ -130,6 +132,14 @@ where = ["src"]
|
|
|
130
132
|
testpaths = ["tests"]
|
|
131
133
|
pythonpath = ["src"]
|
|
132
134
|
|
|
135
|
+
[tool.coverage.run]
|
|
136
|
+
source = ["ipmg"]
|
|
137
|
+
branch = true
|
|
138
|
+
|
|
139
|
+
[tool.coverage.report]
|
|
140
|
+
# Current coverage is ~95%; this floor catches a large untested addition.
|
|
141
|
+
fail_under = 90
|
|
142
|
+
|
|
133
143
|
|
|
134
144
|
# ==========================================================
|
|
135
145
|
# Ruff
|
|
@@ -7,6 +7,12 @@ import argparse
|
|
|
7
7
|
from ipmg import __version__
|
|
8
8
|
from ipmg.core.portscan import DEFAULT_PORTS, parse_port_list
|
|
9
9
|
from ipmg.infrastructure.file_io import DEFAULT_INPUT_FILE
|
|
10
|
+
from ipmg.infrastructure.incremental import (
|
|
11
|
+
DEFAULT_AUTOSAVE_S,
|
|
12
|
+
MAX_AUTOSAVE_S,
|
|
13
|
+
MIN_AUTOSAVE_S,
|
|
14
|
+
REPORT_FORMATS,
|
|
15
|
+
)
|
|
10
16
|
from ipmg.reporting.diff_report import DIFF_FORMATS
|
|
11
17
|
from ipmg.reporting.live import DEFAULT_REFRESH_S, MAX_REFRESH_S, MIN_REFRESH_S
|
|
12
18
|
|
|
@@ -135,9 +141,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
135
141
|
"--formats",
|
|
136
142
|
nargs="+",
|
|
137
143
|
default=["xlsx"],
|
|
138
|
-
choices=
|
|
144
|
+
choices=list(REPORT_FORMATS),
|
|
139
145
|
metavar="FORMAT",
|
|
140
|
-
help="One or more report formats:
|
|
146
|
+
help=f"One or more report formats: {', '.join(REPORT_FORMATS)} (default: xlsx).",
|
|
141
147
|
)
|
|
142
148
|
parser.add_argument(
|
|
143
149
|
"--discover",
|
|
@@ -196,6 +202,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
196
202
|
help="Connect timeout per port when --scan-ports is set (default: 1).",
|
|
197
203
|
)
|
|
198
204
|
|
|
205
|
+
reports = parser.add_argument_group("report writing")
|
|
206
|
+
reports.add_argument(
|
|
207
|
+
"--no-incremental",
|
|
208
|
+
action="store_true",
|
|
209
|
+
help="Only write the report once the scan has finished.",
|
|
210
|
+
)
|
|
211
|
+
reports.add_argument(
|
|
212
|
+
"--autosave",
|
|
213
|
+
type=float,
|
|
214
|
+
default=DEFAULT_AUTOSAVE_S,
|
|
215
|
+
metavar="SECONDS",
|
|
216
|
+
help=(
|
|
217
|
+
"How often a running scan re-saves the xlsx, json, and md reports "
|
|
218
|
+
f"(default: {DEFAULT_AUTOSAVE_S:g}, range: {MIN_AUTOSAVE_S:g}-{MAX_AUTOSAVE_S:g}). "
|
|
219
|
+
"csv and jsonl are written per host regardless."
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
199
223
|
live = parser.add_argument_group("live output")
|
|
200
224
|
live.add_argument(
|
|
201
225
|
"--stream",
|
|
@@ -95,6 +95,12 @@ def execute_scan(
|
|
|
95
95
|
|
|
96
96
|
if on_result is not None:
|
|
97
97
|
on_result(result, len(results), len(ips))
|
|
98
|
+
except BaseException:
|
|
99
|
+
# Ctrl+C must not be followed by minutes of queued pings: drop the
|
|
100
|
+
# hosts that have not started, and let the finally below wait only
|
|
101
|
+
# for the handful already in flight.
|
|
102
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
103
|
+
raise
|
|
98
104
|
finally:
|
|
99
105
|
executor.shutdown(wait=True)
|
|
100
106
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
1
4
|
import ipaddress
|
|
2
5
|
from pathlib import Path
|
|
3
6
|
from typing import Iterable
|
|
@@ -6,6 +9,7 @@ import pandas as pd
|
|
|
6
9
|
|
|
7
10
|
from ipmg.core.ping import validate_ip
|
|
8
11
|
from ipmg.exceptions import FileIOError
|
|
12
|
+
from ipmg.infrastructure.incremental import atomic_write_bytes, jsonl_record
|
|
9
13
|
from ipmg.reporting import ui
|
|
10
14
|
from ipmg.utils.helpers import markdown_cell, spreadsheet_escape, timestamp_str
|
|
11
15
|
|
|
@@ -270,24 +274,50 @@ def build_markdown_report(df: pd.DataFrame) -> str:
|
|
|
270
274
|
return "\n".join(lines)
|
|
271
275
|
|
|
272
276
|
|
|
273
|
-
def
|
|
274
|
-
|
|
277
|
+
def write_report(df: pd.DataFrame, path: str, fmt: str) -> None:
|
|
278
|
+
"""Write ``df`` to ``path`` in one format, replacing the file atomically.
|
|
279
|
+
|
|
280
|
+
The whole file is rendered in memory and swapped into place, so a reader
|
|
281
|
+
— or an incremental snapshot interrupted halfway — never finds a report
|
|
282
|
+
that is only partly written. Raises :class:`ValueError` for an unknown
|
|
283
|
+
format so callers can tell "not written" from "silently skipped".
|
|
284
|
+
"""
|
|
285
|
+
if fmt == "xlsx":
|
|
286
|
+
buffer = io.BytesIO()
|
|
287
|
+
sanitize_export_frame(df).to_excel(buffer, index=False)
|
|
288
|
+
data = buffer.getvalue()
|
|
289
|
+
elif fmt == "csv":
|
|
290
|
+
data = sanitize_export_frame(df).to_csv(index=False).encode("utf-8")
|
|
291
|
+
elif fmt == "json":
|
|
292
|
+
data = df.to_json(orient="records").encode("utf-8")
|
|
293
|
+
elif fmt == "jsonl":
|
|
294
|
+
# Rendered row by row rather than through pandas, so the finished file
|
|
295
|
+
# is written exactly like the one a running scan appends to.
|
|
296
|
+
rows = df.astype(object).where(pd.notna(df), None).to_dict(orient="records")
|
|
297
|
+
data = "".join(jsonl_record(row) for row in rows).encode("utf-8")
|
|
298
|
+
elif fmt == "md":
|
|
299
|
+
data = build_markdown_report(df).encode("utf-8")
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError(f"Unsupported report format: {fmt}")
|
|
302
|
+
|
|
303
|
+
atomic_write_bytes(path, data)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def save_results(df, base: str, formats: list[str], timestamp: str | None = None) -> list[str]:
|
|
307
|
+
"""Write the finished report in every requested format.
|
|
308
|
+
|
|
309
|
+
``timestamp`` is passed in when a scan has already been writing reports
|
|
310
|
+
incrementally, so the final write lands on those same files instead of
|
|
311
|
+
creating a second, differently named set.
|
|
312
|
+
"""
|
|
313
|
+
ts = timestamp or timestamp_str()
|
|
275
314
|
saved_paths: list[str] = []
|
|
276
315
|
|
|
277
316
|
for fmt in formats:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
output_path = f"{base}_{ts}.csv"
|
|
283
|
-
sanitize_export_frame(df).to_csv(output_path, index=False)
|
|
284
|
-
elif fmt == "json":
|
|
285
|
-
output_path = f"{base}_{ts}.json"
|
|
286
|
-
df.to_json(output_path, orient="records")
|
|
287
|
-
elif fmt == "md":
|
|
288
|
-
output_path = f"{base}_{ts}.md"
|
|
289
|
-
Path(output_path).write_text(build_markdown_report(df), encoding="utf-8")
|
|
290
|
-
else:
|
|
317
|
+
output_path = f"{base}_{ts}.{fmt}"
|
|
318
|
+
try:
|
|
319
|
+
write_report(df, output_path, fmt)
|
|
320
|
+
except ValueError:
|
|
291
321
|
continue
|
|
292
322
|
|
|
293
323
|
saved_paths.append(output_path)
|