ipmg 1.13.2__tar.gz → 2.0.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-1.13.2/src/ipmg.egg-info → ipmg-2.0.0}/PKG-INFO +26 -15
- {ipmg-1.13.2 → ipmg-2.0.0}/README.md +25 -14
- {ipmg-1.13.2 → ipmg-2.0.0}/pyproject.toml +1 -1
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/__init__.py +1 -1
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/cli/commands.py +26 -8
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/cli/parser.py +81 -19
- ipmg-2.0.0/src/ipmg/web/__init__.py +1 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/app.py +1 -1
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/index.html +1 -1
- {ipmg-1.13.2 → ipmg-2.0.0/src/ipmg.egg-info}/PKG-INFO +26 -15
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_commands.py +56 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_web_api.py +1 -1
- ipmg-1.13.2/src/ipmg/web/__init__.py +0 -1
- {ipmg-1.13.2 → ipmg-2.0.0}/LICENSE +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/setup.cfg +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/__main__.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/cli/__init__.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/__init__.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/diff.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/discovery.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/engine.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/ping.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/portscan.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/security.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/exceptions.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/infrastructure/__init__.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/infrastructure/database.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/infrastructure/file_io.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/__init__.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/diff_report.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/frames.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/live.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/summary.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/ui.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/services/__init__.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/services/history_service.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/services/scan_service.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/utils/__init__.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/utils/helpers.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/db.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/manager.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/server.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/css/app.css +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/api.js +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/app.js +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/charts.js +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/demo.js +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/views.js +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/SOURCES.txt +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/dependency_links.txt +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/entry_points.txt +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/requires.txt +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/top_level.txt +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_database_history.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_diff.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_diff_report.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_discover.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_engine.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_file_io.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_history_service.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_live.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_parser.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_ping.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_ping_command.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_portscan.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_scan_service.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_ui.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_utils.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_web_db.py +0 -0
- {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_web_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipmg
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: IP Management & Ping Monitoring CLI Tool
|
|
5
5
|
Author: Sameer Alam
|
|
6
6
|
Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
|
|
@@ -55,11 +55,11 @@ Dynamic: license-file
|
|
|
55
55
|
|
|
56
56
|
IPMG pings hosts in parallel, resolves their names, and hands you a report you
|
|
57
57
|
can send to someone: Excel, CSV, JSON, or Markdown. It works from the command
|
|
58
|
-
line or from a local
|
|
58
|
+
line or from IPMG Web, a local browser UI, and it remembers every scan so it can tell
|
|
59
59
|
you what moved.
|
|
60
60
|
|
|
61
61
|
**Website:** [sameeralam3127.github.io/ipmg](https://sameeralam3127.github.io/ipmg/) ·
|
|
62
|
-
**Live demo:** [
|
|
62
|
+
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/)
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
65
|
pip install ipmg
|
|
@@ -76,7 +76,7 @@ ipmg --discover # scan the network you are on, right now
|
|
|
76
76
|
[Common tasks](#common-tasks) ·
|
|
77
77
|
[Live results](#live-results) ·
|
|
78
78
|
[Change detection](#change-detection) ·
|
|
79
|
-
[Web
|
|
79
|
+
[IPMG Web](#ipmg-web) ·
|
|
80
80
|
[What you can scan](#what-you-can-scan) ·
|
|
81
81
|
[Reports](#reports) ·
|
|
82
82
|
[All options](#all-options) ·
|
|
@@ -105,6 +105,17 @@ the handful of system packages it needs (`curl`, `tar`, `gzip`, `ping`) for you:
|
|
|
105
105
|
curl -sSL https://raw.githubusercontent.com/sameeralam3127/ipmg/main/install.sh | bash -s -- --with-deps
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
**macOS and Linux with Homebrew:**
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
brew install sameeralam3127/tap/ipmg
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The formula lives in
|
|
115
|
+
[sameeralam3127/homebrew-tap](https://github.com/sameeralam3127/homebrew-tap)
|
|
116
|
+
and is updated with every release. Upgrade with `brew upgrade ipmg`, remove
|
|
117
|
+
with `brew uninstall ipmg`.
|
|
118
|
+
|
|
108
119
|
**Windows — two commands in PowerShell:**
|
|
109
120
|
|
|
110
121
|
```powershell
|
|
@@ -281,7 +292,7 @@ exist yet. A file you name with `--input` must already exist.
|
|
|
281
292
|
| Keep scanning every 5 minutes | `ipmg --input targets.txt --interval 5` |
|
|
282
293
|
| Look back at earlier scans | `ipmg history` |
|
|
283
294
|
| Compare two specific scans | `ipmg diff 12 14` |
|
|
284
|
-
| Use
|
|
295
|
+
| Use IPMG Web in your browser instead | `ipmg web` |
|
|
285
296
|
| See every available flag | `ipmg --help` |
|
|
286
297
|
|
|
287
298
|
---
|
|
@@ -320,7 +331,7 @@ plain lines, which makes `ipmg --stream-all >> scan.log` a usable live log.
|
|
|
320
331
|
## Change detection
|
|
321
332
|
|
|
322
333
|
Every scan is stored in a local SQLite history (`~/.ipmg/dashboard.db`), shared
|
|
323
|
-
by the CLI and
|
|
334
|
+
by the CLI and IPMG Web. IPMG can then tell you what moved between any two
|
|
324
335
|
scans — which is usually the question you actually have.
|
|
325
336
|
|
|
326
337
|
```bash
|
|
@@ -368,15 +379,17 @@ source**, so file-based and `--discover` runs do not get mixed up. Pass
|
|
|
368
379
|
|
|
369
380
|
---
|
|
370
381
|
|
|
371
|
-
## Web
|
|
382
|
+
## IPMG Web
|
|
372
383
|
|
|
373
|
-
Prefer clicking to typing?
|
|
384
|
+
Prefer clicking to typing? IPMG Web runs locally and shares the CLI's
|
|
374
385
|
scanning engine:
|
|
375
386
|
|
|
376
387
|
```bash
|
|
377
|
-
ipmg
|
|
388
|
+
ipmg web # starts http://127.0.0.1:8080 and opens your browser
|
|
378
389
|
```
|
|
379
390
|
|
|
391
|
+
`ipmg --web` does the same thing, so whichever one you reach for first works.
|
|
392
|
+
|
|
380
393
|
It runs fully offline — every stylesheet and script is bundled with the
|
|
381
394
|
package, nothing is loaded from a CDN. It gives you:
|
|
382
395
|
|
|
@@ -397,13 +410,11 @@ package, nothing is loaded from a CDN. It gives you:
|
|
|
397
410
|
| `--no-browser` | off | Don't open the browser automatically |
|
|
398
411
|
| `--db` | `~/.ipmg/dashboard.db` | History database location |
|
|
399
412
|
|
|
400
|
-
`ipmg web` is an alias for `ipmg dashboard`.
|
|
401
|
-
|
|
402
413
|
### On a server with no browser
|
|
403
414
|
|
|
404
415
|
On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
|
|
405
416
|
that no browser can be opened, skips the attempt, and prints a hint instead of
|
|
406
|
-
failing silently.
|
|
417
|
+
failing silently. IPMG Web still binds to `127.0.0.1` by default, so reach
|
|
407
418
|
it from your workstation with an SSH tunnel:
|
|
408
419
|
|
|
409
420
|
```bash
|
|
@@ -542,10 +553,10 @@ itself is hardened accordingly:
|
|
|
542
553
|
|
|
543
554
|
- Pings run as a direct process call (no shell), and every target is
|
|
544
555
|
validated as an IP address first
|
|
545
|
-
-
|
|
556
|
+
- IPMG Web binds to `127.0.0.1` by default and serves everything
|
|
546
557
|
locally — no CDN assets, no outbound requests
|
|
547
558
|
- WebSocket connections are origin-checked, so a web page you happen to
|
|
548
|
-
visit cannot connect to
|
|
559
|
+
visit cannot connect to your local IPMG Web and read your scan results
|
|
549
560
|
- Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
|
|
550
561
|
so a bad input file cannot exhaust memory
|
|
551
562
|
- All database access uses parameterized SQL
|
|
@@ -579,7 +590,7 @@ Contributions are welcome.
|
|
|
579
590
|
[CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/CONTRIBUTING.md)
|
|
580
591
|
covers setting up a development environment, running the tests, the commit
|
|
581
592
|
message format that drives automated releases, and how the project website and
|
|
582
|
-
|
|
593
|
+
IPMG Web demo are built. Everyone taking part is expected to follow the
|
|
583
594
|
[Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/CODE_OF_CONDUCT.md).
|
|
584
595
|
|
|
585
596
|
---
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
IPMG pings hosts in parallel, resolves their names, and hands you a report you
|
|
11
11
|
can send to someone: Excel, CSV, JSON, or Markdown. It works from the command
|
|
12
|
-
line or from a local
|
|
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
15
|
**Website:** [sameeralam3127.github.io/ipmg](https://sameeralam3127.github.io/ipmg/) ·
|
|
16
|
-
**Live demo:** [
|
|
16
|
+
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/)
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
pip install ipmg
|
|
@@ -30,7 +30,7 @@ ipmg --discover # scan the network you are on, right now
|
|
|
30
30
|
[Common tasks](#common-tasks) ·
|
|
31
31
|
[Live results](#live-results) ·
|
|
32
32
|
[Change detection](#change-detection) ·
|
|
33
|
-
[Web
|
|
33
|
+
[IPMG Web](#ipmg-web) ·
|
|
34
34
|
[What you can scan](#what-you-can-scan) ·
|
|
35
35
|
[Reports](#reports) ·
|
|
36
36
|
[All options](#all-options) ·
|
|
@@ -59,6 +59,17 @@ the handful of system packages it needs (`curl`, `tar`, `gzip`, `ping`) for you:
|
|
|
59
59
|
curl -sSL https://raw.githubusercontent.com/sameeralam3127/ipmg/main/install.sh | bash -s -- --with-deps
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
**macOS and Linux with Homebrew:**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
brew install sameeralam3127/tap/ipmg
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The formula lives in
|
|
69
|
+
[sameeralam3127/homebrew-tap](https://github.com/sameeralam3127/homebrew-tap)
|
|
70
|
+
and is updated with every release. Upgrade with `brew upgrade ipmg`, remove
|
|
71
|
+
with `brew uninstall ipmg`.
|
|
72
|
+
|
|
62
73
|
**Windows — two commands in PowerShell:**
|
|
63
74
|
|
|
64
75
|
```powershell
|
|
@@ -235,7 +246,7 @@ exist yet. A file you name with `--input` must already exist.
|
|
|
235
246
|
| Keep scanning every 5 minutes | `ipmg --input targets.txt --interval 5` |
|
|
236
247
|
| Look back at earlier scans | `ipmg history` |
|
|
237
248
|
| Compare two specific scans | `ipmg diff 12 14` |
|
|
238
|
-
| Use
|
|
249
|
+
| Use IPMG Web in your browser instead | `ipmg web` |
|
|
239
250
|
| See every available flag | `ipmg --help` |
|
|
240
251
|
|
|
241
252
|
---
|
|
@@ -274,7 +285,7 @@ plain lines, which makes `ipmg --stream-all >> scan.log` a usable live log.
|
|
|
274
285
|
## Change detection
|
|
275
286
|
|
|
276
287
|
Every scan is stored in a local SQLite history (`~/.ipmg/dashboard.db`), shared
|
|
277
|
-
by the CLI and
|
|
288
|
+
by the CLI and IPMG Web. IPMG can then tell you what moved between any two
|
|
278
289
|
scans — which is usually the question you actually have.
|
|
279
290
|
|
|
280
291
|
```bash
|
|
@@ -322,15 +333,17 @@ source**, so file-based and `--discover` runs do not get mixed up. Pass
|
|
|
322
333
|
|
|
323
334
|
---
|
|
324
335
|
|
|
325
|
-
## Web
|
|
336
|
+
## IPMG Web
|
|
326
337
|
|
|
327
|
-
Prefer clicking to typing?
|
|
338
|
+
Prefer clicking to typing? IPMG Web runs locally and shares the CLI's
|
|
328
339
|
scanning engine:
|
|
329
340
|
|
|
330
341
|
```bash
|
|
331
|
-
ipmg
|
|
342
|
+
ipmg web # starts http://127.0.0.1:8080 and opens your browser
|
|
332
343
|
```
|
|
333
344
|
|
|
345
|
+
`ipmg --web` does the same thing, so whichever one you reach for first works.
|
|
346
|
+
|
|
334
347
|
It runs fully offline — every stylesheet and script is bundled with the
|
|
335
348
|
package, nothing is loaded from a CDN. It gives you:
|
|
336
349
|
|
|
@@ -351,13 +364,11 @@ package, nothing is loaded from a CDN. It gives you:
|
|
|
351
364
|
| `--no-browser` | off | Don't open the browser automatically |
|
|
352
365
|
| `--db` | `~/.ipmg/dashboard.db` | History database location |
|
|
353
366
|
|
|
354
|
-
`ipmg web` is an alias for `ipmg dashboard`.
|
|
355
|
-
|
|
356
367
|
### On a server with no browser
|
|
357
368
|
|
|
358
369
|
On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
|
|
359
370
|
that no browser can be opened, skips the attempt, and prints a hint instead of
|
|
360
|
-
failing silently.
|
|
371
|
+
failing silently. IPMG Web still binds to `127.0.0.1` by default, so reach
|
|
361
372
|
it from your workstation with an SSH tunnel:
|
|
362
373
|
|
|
363
374
|
```bash
|
|
@@ -496,10 +507,10 @@ itself is hardened accordingly:
|
|
|
496
507
|
|
|
497
508
|
- Pings run as a direct process call (no shell), and every target is
|
|
498
509
|
validated as an IP address first
|
|
499
|
-
-
|
|
510
|
+
- IPMG Web binds to `127.0.0.1` by default and serves everything
|
|
500
511
|
locally — no CDN assets, no outbound requests
|
|
501
512
|
- WebSocket connections are origin-checked, so a web page you happen to
|
|
502
|
-
visit cannot connect to
|
|
513
|
+
visit cannot connect to your local IPMG Web and read your scan results
|
|
503
514
|
- Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
|
|
504
515
|
so a bad input file cannot exhaust memory
|
|
505
516
|
- All database access uses parameterized SQL
|
|
@@ -533,7 +544,7 @@ Contributions are welcome.
|
|
|
533
544
|
[CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/CONTRIBUTING.md)
|
|
534
545
|
covers setting up a development environment, running the tests, the commit
|
|
535
546
|
message format that drives automated releases, and how the project website and
|
|
536
|
-
|
|
547
|
+
IPMG Web demo are built. Everyone taking part is expected to follow the
|
|
537
548
|
[Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/CODE_OF_CONDUCT.md).
|
|
538
549
|
|
|
539
550
|
---
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "ipmg"
|
|
16
|
-
version = "
|
|
16
|
+
version = "2.0.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"
|
|
@@ -7,10 +7,10 @@ import sys
|
|
|
7
7
|
from typing import Callable, Dict, List, Optional
|
|
8
8
|
|
|
9
9
|
from ipmg.cli.parser import (
|
|
10
|
-
build_dashboard_parser,
|
|
11
10
|
build_diff_parser,
|
|
12
11
|
build_history_parser,
|
|
13
12
|
build_parser,
|
|
13
|
+
build_web_parser,
|
|
14
14
|
)
|
|
15
15
|
from ipmg.core.diff import DiffOptions
|
|
16
16
|
from ipmg.core.security import print_disclaimer_once
|
|
@@ -39,10 +39,10 @@ def _scan_command(argv: List[str]) -> int:
|
|
|
39
39
|
return EXIT_OK
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def
|
|
43
|
-
args =
|
|
42
|
+
def _web_command(argv: List[str]) -> int:
|
|
43
|
+
args = build_web_parser().parse_args(argv)
|
|
44
44
|
configure_logging(args.verbose)
|
|
45
|
-
ui.header("
|
|
45
|
+
ui.header("web")
|
|
46
46
|
print_disclaimer_once()
|
|
47
47
|
|
|
48
48
|
# Imported lazily so plain CLI scans do not pay for the web stack.
|
|
@@ -100,19 +100,37 @@ def _diff_command(argv: List[str]) -> int:
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
_COMMANDS: Dict[str, Callable[[List[str]], int]] = {
|
|
103
|
-
"
|
|
104
|
-
"web": _dashboard_command,
|
|
103
|
+
"web": _web_command,
|
|
105
104
|
"history": _history_command,
|
|
106
105
|
"diff": _diff_command,
|
|
107
106
|
"compare": _diff_command,
|
|
108
107
|
}
|
|
109
108
|
|
|
109
|
+
#: Flag spellings of a command. Reaching for "--web" before "web" is the
|
|
110
|
+
#: obvious guess, and refusing it teaches nothing.
|
|
111
|
+
_COMMAND_FLAGS: Dict[str, Callable[[List[str]], int]] = {
|
|
112
|
+
"--web": _web_command,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _dispatch(argv: List[str]):
|
|
117
|
+
"""Pick the handler for this command line, and the arguments it keeps."""
|
|
118
|
+
if argv and argv[0] in _COMMANDS:
|
|
119
|
+
return _COMMANDS[argv[0]], argv[1:]
|
|
120
|
+
|
|
121
|
+
for index, argument in enumerate(argv):
|
|
122
|
+
if argument in _COMMAND_FLAGS:
|
|
123
|
+
# Everything else is passed through, so `ipmg --web --port 9000`
|
|
124
|
+
# reaches the web parser intact.
|
|
125
|
+
return _COMMAND_FLAGS[argument], argv[:index] + argv[index + 1 :]
|
|
126
|
+
|
|
127
|
+
return _scan_command, argv
|
|
128
|
+
|
|
110
129
|
|
|
111
130
|
def run(argv: Optional[List[str]] = None) -> int:
|
|
112
131
|
"""Entry point: returns the process exit code."""
|
|
113
132
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
114
|
-
|
|
115
|
-
handler, handler_argv = (command, argv[1:]) if command else (_scan_command, argv)
|
|
133
|
+
handler, handler_argv = _dispatch(argv)
|
|
116
134
|
|
|
117
135
|
try:
|
|
118
136
|
return handler(handler_argv)
|
|
@@ -12,6 +12,23 @@ from ipmg.reporting.live import DEFAULT_REFRESH_S, MAX_REFRESH_S, MIN_REFRESH_S
|
|
|
12
12
|
|
|
13
13
|
PROG = "IPMG - IP Management & Ping Monitoring Tool"
|
|
14
14
|
|
|
15
|
+
#: Subcommands, named here so an argument error can point at them. argparse
|
|
16
|
+
#: prints the epilog on --help but never on an error, which is exactly when
|
|
17
|
+
#: someone who typed an unknown flag needs to see them.
|
|
18
|
+
COMMANDS_HINT = (
|
|
19
|
+
"Commands: 'ipmg web' (IPMG Web, the browser UI), 'ipmg history' (stored scans), "
|
|
20
|
+
"'ipmg diff' (compare two scans)."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ScanParser(argparse.ArgumentParser):
|
|
25
|
+
"""Argument parser that mentions the subcommands when a flag misses."""
|
|
26
|
+
|
|
27
|
+
def error(self, message: str): # pragma: no cover - exercised via parse_args
|
|
28
|
+
if "unrecognized arguments" in message:
|
|
29
|
+
message = f"{message}\n\n{COMMANDS_HINT}"
|
|
30
|
+
super().error(message)
|
|
31
|
+
|
|
15
32
|
|
|
16
33
|
def _port_list(value: str) -> tuple:
|
|
17
34
|
try:
|
|
@@ -65,41 +82,73 @@ def _add_diff_export_arguments(parser) -> None:
|
|
|
65
82
|
|
|
66
83
|
|
|
67
84
|
def build_parser() -> argparse.ArgumentParser:
|
|
68
|
-
parser =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"'ipmg diff' (compare two scans)."
|
|
73
|
-
),
|
|
85
|
+
parser = ScanParser(
|
|
86
|
+
prog="ipmg",
|
|
87
|
+
description=PROG,
|
|
88
|
+
epilog=COMMANDS_HINT,
|
|
74
89
|
)
|
|
75
90
|
parser.add_argument(
|
|
76
91
|
"--version",
|
|
77
92
|
action="version",
|
|
78
|
-
version=f"
|
|
93
|
+
version=f"{PROG} {__version__}",
|
|
79
94
|
)
|
|
80
95
|
# No default here, so the scan service can tell "no --input given" (use the
|
|
81
96
|
# sample file) apart from an explicit file name that does not exist (error).
|
|
82
97
|
parser.add_argument(
|
|
83
98
|
"--input",
|
|
84
99
|
default=None,
|
|
100
|
+
metavar="TARGETS",
|
|
85
101
|
help=(
|
|
86
102
|
"IP, CIDR block, range, or target file (.txt, .list, .csv, .xls, .xlsx). "
|
|
87
103
|
f"Without --input or --discover, {DEFAULT_INPUT_FILE} is used and "
|
|
88
104
|
"created with sample targets if it does not exist."
|
|
89
105
|
),
|
|
90
106
|
)
|
|
91
|
-
parser.add_argument(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--output",
|
|
109
|
+
default="results",
|
|
110
|
+
metavar="PREFIX",
|
|
111
|
+
help="Report file name prefix; the timestamp is appended (default: results).",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--timeout",
|
|
115
|
+
type=int,
|
|
116
|
+
default=2,
|
|
117
|
+
metavar="SECONDS",
|
|
118
|
+
help="Seconds to wait for a reply from each host (default: 2).",
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--count",
|
|
122
|
+
type=int,
|
|
123
|
+
default=1,
|
|
124
|
+
metavar="N",
|
|
125
|
+
help="Pings per host; raise it on a lossy link (default: 1).",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--threads",
|
|
129
|
+
type=int,
|
|
130
|
+
default=50,
|
|
131
|
+
metavar="N",
|
|
132
|
+
help="How many hosts to probe at once (default: 50).",
|
|
133
|
+
)
|
|
95
134
|
parser.add_argument(
|
|
96
135
|
"--formats",
|
|
97
136
|
nargs="+",
|
|
98
137
|
default=["xlsx"],
|
|
99
138
|
choices=["xlsx", "csv", "json", "md"],
|
|
139
|
+
metavar="FORMAT",
|
|
140
|
+
help="One or more report formats: xlsx, csv, json, md (default: xlsx).",
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"--discover",
|
|
144
|
+
action="store_true",
|
|
145
|
+
help="Auto-detect this machine's subnet and scan it instead of --input.",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"--resolve",
|
|
149
|
+
action="store_true",
|
|
150
|
+
help="Look up each host's name with a reverse DNS (PTR) query.",
|
|
100
151
|
)
|
|
101
|
-
parser.add_argument("--discover", action="store_true")
|
|
102
|
-
parser.add_argument("--resolve", action="store_true")
|
|
103
152
|
parser.add_argument(
|
|
104
153
|
"--dns-cache-ttl",
|
|
105
154
|
type=int,
|
|
@@ -107,8 +156,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
107
156
|
metavar="SECONDS",
|
|
108
157
|
help="Cache reverse DNS results for this many seconds (default: 300).",
|
|
109
158
|
)
|
|
110
|
-
parser.add_argument(
|
|
111
|
-
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--interval",
|
|
161
|
+
type=int,
|
|
162
|
+
metavar="MINUTES",
|
|
163
|
+
help="Repeat the whole scan every N minutes until interrupted.",
|
|
164
|
+
)
|
|
165
|
+
parser.add_argument("--verbose", action="store_true", help="Debug logging.")
|
|
166
|
+
|
|
167
|
+
# People reach for a flag before a subcommand, so accept both spellings
|
|
168
|
+
# rather than answering "unrecognized arguments: --web".
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--web",
|
|
171
|
+
action="store_true",
|
|
172
|
+
help="Start IPMG Web, the local browser UI (same as 'ipmg web').",
|
|
173
|
+
)
|
|
112
174
|
|
|
113
175
|
ports_group = parser.add_argument_group("TCP service discovery")
|
|
114
176
|
ports_group.add_argument(
|
|
@@ -180,10 +242,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
180
242
|
return parser
|
|
181
243
|
|
|
182
244
|
|
|
183
|
-
def
|
|
245
|
+
def build_web_parser() -> argparse.ArgumentParser:
|
|
184
246
|
parser = argparse.ArgumentParser(
|
|
185
|
-
"ipmg
|
|
186
|
-
description="Start the local
|
|
247
|
+
"ipmg web",
|
|
248
|
+
description="Start IPMG Web, the local browser UI.",
|
|
187
249
|
)
|
|
188
250
|
parser.add_argument(
|
|
189
251
|
"--host",
|
|
@@ -199,7 +261,7 @@ def build_dashboard_parser() -> argparse.ArgumentParser:
|
|
|
199
261
|
parser.add_argument(
|
|
200
262
|
"--no-browser",
|
|
201
263
|
action="store_true",
|
|
202
|
-
help="Do not open
|
|
264
|
+
help="Do not open IPMG Web in a browser automatically.",
|
|
203
265
|
)
|
|
204
266
|
_add_database_argument(parser)
|
|
205
267
|
parser.add_argument("--verbose", action="store_true")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""IPMG Web: the local browser UI for IPMG (FastAPI backend + bundled frontend)."""
|
|
@@ -373,7 +373,7 @@ def create_app(db: Optional[Database] = None) -> FastAPI:
|
|
|
373
373
|
manager.attach_loop(asyncio.get_running_loop())
|
|
374
374
|
yield
|
|
375
375
|
|
|
376
|
-
app = FastAPI(title="IPMG
|
|
376
|
+
app = FastAPI(title="IPMG Web", version=__version__, lifespan=lifespan)
|
|
377
377
|
app.state.db = database
|
|
378
378
|
app.state.manager = manager
|
|
379
379
|
app.state.history = history
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>IPMG
|
|
6
|
+
<title>IPMG Web</title>
|
|
7
7
|
<link rel="stylesheet" href="css/app.css?v=20260911.1">
|
|
8
8
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='13' fill='none' stroke='%232a78d6' stroke-width='3'/%3E%3Ccircle cx='16' cy='16' r='5' fill='%232a78d6'/%3E%3C/svg%3E">
|
|
9
9
|
</head>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipmg
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: IP Management & Ping Monitoring CLI Tool
|
|
5
5
|
Author: Sameer Alam
|
|
6
6
|
Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
|
|
@@ -55,11 +55,11 @@ Dynamic: license-file
|
|
|
55
55
|
|
|
56
56
|
IPMG pings hosts in parallel, resolves their names, and hands you a report you
|
|
57
57
|
can send to someone: Excel, CSV, JSON, or Markdown. It works from the command
|
|
58
|
-
line or from a local
|
|
58
|
+
line or from IPMG Web, a local browser UI, and it remembers every scan so it can tell
|
|
59
59
|
you what moved.
|
|
60
60
|
|
|
61
61
|
**Website:** [sameeralam3127.github.io/ipmg](https://sameeralam3127.github.io/ipmg/) ·
|
|
62
|
-
**Live demo:** [
|
|
62
|
+
**Live demo:** [IPMG Web with sample data](https://sameeralam3127.github.io/ipmg/demo/)
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
65
|
pip install ipmg
|
|
@@ -76,7 +76,7 @@ ipmg --discover # scan the network you are on, right now
|
|
|
76
76
|
[Common tasks](#common-tasks) ·
|
|
77
77
|
[Live results](#live-results) ·
|
|
78
78
|
[Change detection](#change-detection) ·
|
|
79
|
-
[Web
|
|
79
|
+
[IPMG Web](#ipmg-web) ·
|
|
80
80
|
[What you can scan](#what-you-can-scan) ·
|
|
81
81
|
[Reports](#reports) ·
|
|
82
82
|
[All options](#all-options) ·
|
|
@@ -105,6 +105,17 @@ the handful of system packages it needs (`curl`, `tar`, `gzip`, `ping`) for you:
|
|
|
105
105
|
curl -sSL https://raw.githubusercontent.com/sameeralam3127/ipmg/main/install.sh | bash -s -- --with-deps
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
**macOS and Linux with Homebrew:**
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
brew install sameeralam3127/tap/ipmg
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The formula lives in
|
|
115
|
+
[sameeralam3127/homebrew-tap](https://github.com/sameeralam3127/homebrew-tap)
|
|
116
|
+
and is updated with every release. Upgrade with `brew upgrade ipmg`, remove
|
|
117
|
+
with `brew uninstall ipmg`.
|
|
118
|
+
|
|
108
119
|
**Windows — two commands in PowerShell:**
|
|
109
120
|
|
|
110
121
|
```powershell
|
|
@@ -281,7 +292,7 @@ exist yet. A file you name with `--input` must already exist.
|
|
|
281
292
|
| Keep scanning every 5 minutes | `ipmg --input targets.txt --interval 5` |
|
|
282
293
|
| Look back at earlier scans | `ipmg history` |
|
|
283
294
|
| Compare two specific scans | `ipmg diff 12 14` |
|
|
284
|
-
| Use
|
|
295
|
+
| Use IPMG Web in your browser instead | `ipmg web` |
|
|
285
296
|
| See every available flag | `ipmg --help` |
|
|
286
297
|
|
|
287
298
|
---
|
|
@@ -320,7 +331,7 @@ plain lines, which makes `ipmg --stream-all >> scan.log` a usable live log.
|
|
|
320
331
|
## Change detection
|
|
321
332
|
|
|
322
333
|
Every scan is stored in a local SQLite history (`~/.ipmg/dashboard.db`), shared
|
|
323
|
-
by the CLI and
|
|
334
|
+
by the CLI and IPMG Web. IPMG can then tell you what moved between any two
|
|
324
335
|
scans — which is usually the question you actually have.
|
|
325
336
|
|
|
326
337
|
```bash
|
|
@@ -368,15 +379,17 @@ source**, so file-based and `--discover` runs do not get mixed up. Pass
|
|
|
368
379
|
|
|
369
380
|
---
|
|
370
381
|
|
|
371
|
-
## Web
|
|
382
|
+
## IPMG Web
|
|
372
383
|
|
|
373
|
-
Prefer clicking to typing?
|
|
384
|
+
Prefer clicking to typing? IPMG Web runs locally and shares the CLI's
|
|
374
385
|
scanning engine:
|
|
375
386
|
|
|
376
387
|
```bash
|
|
377
|
-
ipmg
|
|
388
|
+
ipmg web # starts http://127.0.0.1:8080 and opens your browser
|
|
378
389
|
```
|
|
379
390
|
|
|
391
|
+
`ipmg --web` does the same thing, so whichever one you reach for first works.
|
|
392
|
+
|
|
380
393
|
It runs fully offline — every stylesheet and script is bundled with the
|
|
381
394
|
package, nothing is loaded from a CDN. It gives you:
|
|
382
395
|
|
|
@@ -397,13 +410,11 @@ package, nothing is loaded from a CDN. It gives you:
|
|
|
397
410
|
| `--no-browser` | off | Don't open the browser automatically |
|
|
398
411
|
| `--db` | `~/.ipmg/dashboard.db` | History database location |
|
|
399
412
|
|
|
400
|
-
`ipmg web` is an alias for `ipmg dashboard`.
|
|
401
|
-
|
|
402
413
|
### On a server with no browser
|
|
403
414
|
|
|
404
415
|
On a Linux server with no display (e.g. accessed over plain SSH), IPMG detects
|
|
405
416
|
that no browser can be opened, skips the attempt, and prints a hint instead of
|
|
406
|
-
failing silently.
|
|
417
|
+
failing silently. IPMG Web still binds to `127.0.0.1` by default, so reach
|
|
407
418
|
it from your workstation with an SSH tunnel:
|
|
408
419
|
|
|
409
420
|
```bash
|
|
@@ -542,10 +553,10 @@ itself is hardened accordingly:
|
|
|
542
553
|
|
|
543
554
|
- Pings run as a direct process call (no shell), and every target is
|
|
544
555
|
validated as an IP address first
|
|
545
|
-
-
|
|
556
|
+
- IPMG Web binds to `127.0.0.1` by default and serves everything
|
|
546
557
|
locally — no CDN assets, no outbound requests
|
|
547
558
|
- WebSocket connections are origin-checked, so a web page you happen to
|
|
548
|
-
visit cannot connect to
|
|
559
|
+
visit cannot connect to your local IPMG Web and read your scan results
|
|
549
560
|
- Uploads are capped at 5 MB and one scan expands to at most 65,536 hosts,
|
|
550
561
|
so a bad input file cannot exhaust memory
|
|
551
562
|
- All database access uses parameterized SQL
|
|
@@ -579,7 +590,7 @@ Contributions are welcome.
|
|
|
579
590
|
[CONTRIBUTING.md](https://github.com/sameeralam3127/ipmg/blob/main/CONTRIBUTING.md)
|
|
580
591
|
covers setting up a development environment, running the tests, the commit
|
|
581
592
|
message format that drives automated releases, and how the project website and
|
|
582
|
-
|
|
593
|
+
IPMG Web demo are built. Everyone taking part is expected to follow the
|
|
583
594
|
[Code of Conduct](https://github.com/sameeralam3127/ipmg/blob/main/CODE_OF_CONDUCT.md).
|
|
584
595
|
|
|
585
596
|
---
|
|
@@ -168,3 +168,59 @@ def test_missing_input_file_is_an_error_not_a_sample(tmp_path, capsys):
|
|
|
168
168
|
assert exit_code == commands.EXIT_ERROR
|
|
169
169
|
assert "was not found" in capsys.readouterr().out
|
|
170
170
|
assert not missing.exists()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def web_calls(monkeypatch):
|
|
174
|
+
"""Capture what run_dashboard would have been started with."""
|
|
175
|
+
calls = []
|
|
176
|
+
monkeypatch.setattr(
|
|
177
|
+
"ipmg.web.server.run_dashboard",
|
|
178
|
+
lambda **kwargs: calls.append(kwargs),
|
|
179
|
+
)
|
|
180
|
+
return calls
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@pytest.mark.parametrize("argv", [["web"], ["--web"]])
|
|
184
|
+
def test_both_spellings_of_web_start_it(argv, monkeypatch):
|
|
185
|
+
"""A flag is the obvious guess; refusing it only teaches frustration."""
|
|
186
|
+
calls = web_calls(monkeypatch)
|
|
187
|
+
|
|
188
|
+
assert commands.run(argv) == commands.EXIT_OK
|
|
189
|
+
assert calls == [{"host": "127.0.0.1", "port": 8080, "open_browser": True, "db_path": None}]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_web_flag_still_takes_the_web_options(monkeypatch):
|
|
193
|
+
calls = web_calls(monkeypatch)
|
|
194
|
+
|
|
195
|
+
assert commands.run(["--web", "--port", "9000", "--no-browser"]) == commands.EXIT_OK
|
|
196
|
+
assert calls[0]["port"] == 9000
|
|
197
|
+
assert calls[0]["open_browser"] is False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@pytest.mark.parametrize("argv", [["dashboard"], ["--dashboard"]])
|
|
201
|
+
def test_dashboard_is_no_longer_a_command(argv, monkeypatch, capsys):
|
|
202
|
+
calls = web_calls(monkeypatch)
|
|
203
|
+
|
|
204
|
+
with pytest.raises(SystemExit):
|
|
205
|
+
commands.run(argv)
|
|
206
|
+
|
|
207
|
+
assert calls == []
|
|
208
|
+
assert "ipmg web" in capsys.readouterr().err
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_scan_flags_are_documented(capsys):
|
|
212
|
+
"""Bare flags with no help text are invisible to anyone reading --help."""
|
|
213
|
+
with pytest.raises(SystemExit):
|
|
214
|
+
commands.run(["--help"])
|
|
215
|
+
|
|
216
|
+
# argparse wraps to the terminal width, so compare against flattened text.
|
|
217
|
+
out = " ".join(capsys.readouterr().out.split())
|
|
218
|
+
for phrase in (
|
|
219
|
+
"Report file name prefix",
|
|
220
|
+
"Seconds to wait",
|
|
221
|
+
"Pings per host",
|
|
222
|
+
"How many hosts to probe",
|
|
223
|
+
"Repeat the whole scan",
|
|
224
|
+
"same as 'ipmg web'",
|
|
225
|
+
):
|
|
226
|
+
assert phrase in out
|
|
@@ -219,7 +219,7 @@ def test_upload_rejects_oversized_files(client, monkeypatch):
|
|
|
219
219
|
def test_index_served(client):
|
|
220
220
|
response = client.get("/")
|
|
221
221
|
assert response.status_code == 200
|
|
222
|
-
assert "IPMG
|
|
222
|
+
assert "IPMG Web" in response.text
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
def test_report_rendering_neutralizes_formula_cells():
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Local web dashboard for IPMG (FastAPI backend + bundled frontend)."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|