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.
Files changed (70) hide show
  1. {ipmg-1.13.2/src/ipmg.egg-info → ipmg-2.0.0}/PKG-INFO +26 -15
  2. {ipmg-1.13.2 → ipmg-2.0.0}/README.md +25 -14
  3. {ipmg-1.13.2 → ipmg-2.0.0}/pyproject.toml +1 -1
  4. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/__init__.py +1 -1
  5. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/cli/commands.py +26 -8
  6. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/cli/parser.py +81 -19
  7. ipmg-2.0.0/src/ipmg/web/__init__.py +1 -0
  8. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/app.py +1 -1
  9. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/index.html +1 -1
  10. {ipmg-1.13.2 → ipmg-2.0.0/src/ipmg.egg-info}/PKG-INFO +26 -15
  11. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_commands.py +56 -0
  12. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_web_api.py +1 -1
  13. ipmg-1.13.2/src/ipmg/web/__init__.py +0 -1
  14. {ipmg-1.13.2 → ipmg-2.0.0}/LICENSE +0 -0
  15. {ipmg-1.13.2 → ipmg-2.0.0}/setup.cfg +0 -0
  16. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/__main__.py +0 -0
  17. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/cli/__init__.py +0 -0
  18. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/__init__.py +0 -0
  19. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/diff.py +0 -0
  20. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/discovery.py +0 -0
  21. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/engine.py +0 -0
  22. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/ping.py +0 -0
  23. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/portscan.py +0 -0
  24. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/core/security.py +0 -0
  25. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/exceptions.py +0 -0
  26. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/infrastructure/__init__.py +0 -0
  27. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/infrastructure/database.py +0 -0
  28. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/infrastructure/file_io.py +0 -0
  29. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/__init__.py +0 -0
  30. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/diff_report.py +0 -0
  31. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/frames.py +0 -0
  32. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/live.py +0 -0
  33. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/summary.py +0 -0
  34. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/reporting/ui.py +0 -0
  35. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/services/__init__.py +0 -0
  36. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/services/history_service.py +0 -0
  37. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/services/scan_service.py +0 -0
  38. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/utils/__init__.py +0 -0
  39. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/utils/helpers.py +0 -0
  40. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/db.py +0 -0
  41. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/manager.py +0 -0
  42. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/server.py +0 -0
  43. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/css/app.css +0 -0
  44. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/api.js +0 -0
  45. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/app.js +0 -0
  46. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/charts.js +0 -0
  47. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/demo.js +0 -0
  48. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg/web/static/js/views.js +0 -0
  49. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/SOURCES.txt +0 -0
  50. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/dependency_links.txt +0 -0
  51. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/entry_points.txt +0 -0
  52. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/requires.txt +0 -0
  53. {ipmg-1.13.2 → ipmg-2.0.0}/src/ipmg.egg-info/top_level.txt +0 -0
  54. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_database_history.py +0 -0
  55. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_diff.py +0 -0
  56. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_diff_report.py +0 -0
  57. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_discover.py +0 -0
  58. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_engine.py +0 -0
  59. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_file_io.py +0 -0
  60. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_history_service.py +0 -0
  61. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_live.py +0 -0
  62. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_parser.py +0 -0
  63. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_ping.py +0 -0
  64. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_ping_command.py +0 -0
  65. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_portscan.py +0 -0
  66. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_scan_service.py +0 -0
  67. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_ui.py +0 -0
  68. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_utils.py +0 -0
  69. {ipmg-1.13.2 → ipmg-2.0.0}/tests/test_web_db.py +0 -0
  70. {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: 1.13.2
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 web dashboard, and it remembers every scan so it can tell
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:** [dashboard with sample data](https://sameeralam3127.github.io/ipmg/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 dashboard](#web-dashboard) ·
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 the web dashboard instead | `ipmg dashboard` |
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 the dashboard. IPMG can then tell you what moved between any two
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 dashboard
382
+ ## IPMG Web
372
383
 
373
- Prefer clicking to typing? The dashboard runs locally and shares the CLI's
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 dashboard # starts http://127.0.0.1:8080 and opens your browser
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. The dashboard still binds to `127.0.0.1` by default, so reach
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
- - The dashboard binds to `127.0.0.1` by default and serves everything
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 the local dashboard and read your scan results
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
- dashboard demo are built. Everyone taking part is expected to follow the
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 web dashboard, and it remembers every scan so it can tell
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:** [dashboard with sample data](https://sameeralam3127.github.io/ipmg/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 dashboard](#web-dashboard) ·
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 the web dashboard instead | `ipmg dashboard` |
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 the dashboard. IPMG can then tell you what moved between any two
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 dashboard
336
+ ## IPMG Web
326
337
 
327
- Prefer clicking to typing? The dashboard runs locally and shares the CLI's
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 dashboard # starts http://127.0.0.1:8080 and opens your browser
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. The dashboard still binds to `127.0.0.1` by default, so reach
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
- - The dashboard binds to `127.0.0.1` by default and serves everything
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 the local dashboard and read your scan results
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
- dashboard demo are built. Everyone taking part is expected to follow the
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 = "1.13.2" # Managed automatically by semantic-release
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"
@@ -2,4 +2,4 @@
2
2
  ipmg - IP Management & Ping Monitoring Tool
3
3
  """
4
4
 
5
- __version__ = "1.13.2"
5
+ __version__ = "2.0.0"
@@ -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 _dashboard_command(argv: List[str]) -> int:
43
- args = build_dashboard_parser().parse_args(argv)
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("dashboard")
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
- "dashboard": _dashboard_command,
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
- command = _COMMANDS.get(argv[0]) if argv else None
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 = argparse.ArgumentParser(
69
- PROG,
70
- epilog=(
71
- "Commands: 'ipmg dashboard' (web UI), 'ipmg history' (stored scans), "
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"%(prog)s {__version__}",
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("--output", default="results")
92
- parser.add_argument("--timeout", type=int, default=2)
93
- parser.add_argument("--count", type=int, default=1)
94
- parser.add_argument("--threads", type=int, default=50)
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("--interval", type=int)
111
- parser.add_argument("--verbose", action="store_true")
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 build_dashboard_parser() -> argparse.ArgumentParser:
245
+ def build_web_parser() -> argparse.ArgumentParser:
184
246
  parser = argparse.ArgumentParser(
185
- "ipmg dashboard",
186
- description="Start the local IPMG web dashboard.",
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 the dashboard in a browser automatically.",
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 Dashboard", version=__version__, lifespan=lifespan)
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 Dashboard</title>
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: 1.13.2
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 web dashboard, and it remembers every scan so it can tell
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:** [dashboard with sample data](https://sameeralam3127.github.io/ipmg/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 dashboard](#web-dashboard) ·
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 the web dashboard instead | `ipmg dashboard` |
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 the dashboard. IPMG can then tell you what moved between any two
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 dashboard
382
+ ## IPMG Web
372
383
 
373
- Prefer clicking to typing? The dashboard runs locally and shares the CLI's
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 dashboard # starts http://127.0.0.1:8080 and opens your browser
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. The dashboard still binds to `127.0.0.1` by default, so reach
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
- - The dashboard binds to `127.0.0.1` by default and serves everything
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 the local dashboard and read your scan results
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
- dashboard demo are built. Everyone taking part is expected to follow the
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 Dashboard" in response.text
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