ipmg 2.3.0__tar.gz → 2.4.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 (82) hide show
  1. {ipmg-2.3.0/src/ipmg.egg-info → ipmg-2.4.0}/PKG-INFO +108 -6
  2. {ipmg-2.3.0 → ipmg-2.4.0}/README.md +106 -5
  3. {ipmg-2.3.0 → ipmg-2.4.0}/pyproject.toml +4 -1
  4. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/__init__.py +1 -1
  5. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/cli/commands.py +11 -2
  6. ipmg-2.4.0/src/ipmg/cli/config.py +410 -0
  7. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/cli/parser.py +40 -6
  8. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/exceptions.py +4 -0
  9. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/infrastructure/file_io.py +91 -5
  10. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/infrastructure/incremental.py +10 -0
  11. ipmg-2.4.0/src/ipmg/reporting/machine.py +88 -0
  12. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/reporting/ui.py +13 -0
  13. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/services/scan_service.py +69 -16
  14. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/utils/helpers.py +10 -7
  15. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/app.py +14 -33
  16. {ipmg-2.3.0 → ipmg-2.4.0/src/ipmg.egg-info}/PKG-INFO +108 -6
  17. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg.egg-info/SOURCES.txt +4 -0
  18. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg.egg-info/requires.txt +3 -0
  19. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_commands.py +1 -1
  20. ipmg-2.4.0/tests/test_config.py +402 -0
  21. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_file_io.py +146 -2
  22. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_incremental.py +8 -7
  23. ipmg-2.4.0/tests/test_machine_output.py +147 -0
  24. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_parser.py +17 -0
  25. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_scan_service.py +44 -10
  26. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_web_api.py +10 -0
  27. {ipmg-2.3.0 → ipmg-2.4.0}/LICENSE +0 -0
  28. {ipmg-2.3.0 → ipmg-2.4.0}/setup.cfg +0 -0
  29. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/__main__.py +0 -0
  30. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/cli/__init__.py +0 -0
  31. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/core/__init__.py +0 -0
  32. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/core/diff.py +0 -0
  33. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/core/discovery.py +0 -0
  34. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/core/engine.py +0 -0
  35. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/core/health.py +0 -0
  36. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/core/ping.py +0 -0
  37. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/core/portscan.py +0 -0
  38. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/core/security.py +0 -0
  39. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/infrastructure/__init__.py +0 -0
  40. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/infrastructure/database.py +0 -0
  41. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/infrastructure/notify.py +0 -0
  42. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/reporting/__init__.py +0 -0
  43. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/reporting/diff_report.py +0 -0
  44. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/reporting/frames.py +0 -0
  45. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/reporting/live.py +0 -0
  46. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/reporting/summary.py +0 -0
  47. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/services/__init__.py +0 -0
  48. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/services/history_service.py +0 -0
  49. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/utils/__init__.py +0 -0
  50. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/__init__.py +0 -0
  51. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/db.py +0 -0
  52. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/manager.py +0 -0
  53. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/schemas.py +0 -0
  54. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/server.py +0 -0
  55. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/static/css/app.css +0 -0
  56. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/static/index.html +0 -0
  57. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/static/js/api.js +0 -0
  58. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/static/js/app.js +0 -0
  59. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/static/js/charts.js +0 -0
  60. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/static/js/demo.js +0 -0
  61. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg/web/static/js/views.js +0 -0
  62. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg.egg-info/dependency_links.txt +0 -0
  63. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg.egg-info/entry_points.txt +0 -0
  64. {ipmg-2.3.0 → ipmg-2.4.0}/src/ipmg.egg-info/top_level.txt +0 -0
  65. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_api_contract.py +0 -0
  66. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_database_history.py +0 -0
  67. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_diff.py +0 -0
  68. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_diff_report.py +0 -0
  69. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_discover.py +0 -0
  70. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_engine.py +0 -0
  71. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_health.py +0 -0
  72. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_history_service.py +0 -0
  73. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_live.py +0 -0
  74. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_notify.py +0 -0
  75. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_ping.py +0 -0
  76. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_ping_command.py +0 -0
  77. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_portscan.py +0 -0
  78. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_ui.py +0 -0
  79. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_utils.py +0 -0
  80. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_web_db.py +0 -0
  81. {ipmg-2.3.0 → ipmg-2.4.0}/tests/test_web_manager.py +0 -0
  82. {ipmg-2.3.0 → ipmg-2.4.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.0
3
+ Version: 2.4.0
4
4
  Summary: IP Management & Ping Monitoring CLI Tool
5
5
  Author: Sameer Alam
6
6
  Maintainer-email: Sameer Alam <sameeralam3127@gmail.com>
@@ -37,6 +37,7 @@ Requires-Dist: pydantic>=2.7
37
37
  Requires-Dist: uvicorn>=0.27
38
38
  Requires-Dist: websockets>=12
39
39
  Requires-Dist: python-multipart>=0.0.9
40
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
40
41
  Provides-Extra: dev
41
42
  Requires-Dist: pytest>=7.4; extra == "dev"
42
43
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
@@ -331,8 +332,10 @@ Or pick a ready-made command:
331
332
  | --- | --- |
332
333
  | Scan the network I am on | `ipmg --discover` |
333
334
  | Scan hosts listed in a file | `ipmg --input targets.txt` |
335
+ | Scan a file plus a few extra hosts | `ipmg --input targets.txt 10.0.0.0/30 10.0.0.5` |
334
336
  | Get names, not just IP addresses | `ipmg --input targets.txt --resolve` |
335
337
  | Get a report I can send to someone | `ipmg --input targets.txt --formats md csv` |
338
+ | Pipe the results into a script | `ipmg --input targets.txt --json \| jq .` |
336
339
  | See hosts appear as they answer | `ipmg --input 192.168.1.0/24 --stream` |
337
340
  | See what changed since last time | `ipmg --input targets.txt --compare` |
338
341
  | Check which services are listening | `ipmg --input targets.txt --scan-ports` |
@@ -534,9 +537,32 @@ Anywhere IPMG takes `--input`, you can give it any of these:
534
537
  | 192.168.1.1 |
535
538
  | 10.0.1.0/30 |
536
539
 
537
- Duplicate targets are removed automatically, and one scan expands to at most
538
- 65,536 hosts — larger CIDR blocks or ranges are rejected up front, before the
539
- scan starts.
540
+ - **A JSON file** (`.json`) — a list of addresses, a list of objects keyed by
541
+ `IP Address`, `ip`, or `target`, or an object with a `targets` or `ips` array:
542
+
543
+ ```json
544
+ ["192.168.1.1", "10.0.1.0/30"]
545
+ ```
546
+
547
+ Because `IP Address` is one of the keys it accepts, a report IPMG wrote with
548
+ `--formats json` can be fed straight back in:
549
+
550
+ ```bash
551
+ ipmg --input results_20260628_120000.json
552
+ ```
553
+
554
+ `--input` takes as many of these as you like, in any mix, and may also be
555
+ repeated:
556
+
557
+ ```bash
558
+ ipmg --input targets.txt 10.0.0.0/30 10.0.0.5
559
+ ipmg --input targets.txt --input 10.0.0.5 # the same thing
560
+ ```
561
+
562
+ Duplicate targets are removed automatically — across sources too, so a host
563
+ that is both in the file and on the command line is scanned once — and one scan
564
+ expands to at most 65,536 hosts in total. Larger CIDR blocks, ranges, or
565
+ combinations are rejected up front, before the scan starts.
540
566
 
541
567
  ---
542
568
 
@@ -586,6 +612,22 @@ preferring `jsonl` or `csv` (current to the last host) over `json` or `xlsx`
586
612
  (current to the last autosave). Hosts dropped from the target list since are
587
613
  left out of the finished report.
588
614
 
615
+ **Piping results into a script.** `--json` prints the finished scan to stdout as
616
+ a JSON array, and `--jsonl` prints one object per host the moment its probe
617
+ finishes. The human output moves to stderr, so stdout is nothing but data and no
618
+ temp file or glob is needed:
619
+
620
+ ```bash
621
+ ipmg --input 10.0.0.0/24 --json | jq '.[] | select(.Status == "Active")'
622
+ ipmg --input 10.0.0.0/24 --jsonl | while read -r host; do notify "$host"; done
623
+ ipmg --input 10.0.0.0/24 --json 2>/dev/null > hosts.json # data only
624
+ ```
625
+
626
+ The field names are the report columns above, and both flags print a host
627
+ identically — `--json` is what `--jsonl` prints, gathered into an array. Unless
628
+ you ask for `--formats` explicitly, a piped scan writes no report file at all.
629
+ Exit codes are unchanged.
630
+
589
631
  **Open ports.** `Open Ports` is only populated when `--scan-ports` is set: for
590
632
  each host that answers, IPMG probes a list of common TCP ports (SSH, HTTP,
591
633
  HTTPS, RDP, SMB, FTP, SMTP, DNS, MSSQL, MySQL, PostgreSQL by default)
@@ -602,6 +644,61 @@ them — so piping IPMG into a file or a log gives you clean text.
602
644
 
603
645
  ---
604
646
 
647
+ ## Default options in a file
648
+
649
+ If every run repeats the same flags, put them in **`ipmg.toml`** next to your
650
+ work instead:
651
+
652
+ ```toml
653
+ threads = 200
654
+ timeout = 1
655
+ resolve = true
656
+ formats = ["md", "csv"]
657
+
658
+ [profile.datacenter]
659
+ threads = 400
660
+ scan-ports = true
661
+ ports = "22,80,443"
662
+ ```
663
+
664
+ ```bash
665
+ ipmg --input targets.txt # uses the file's defaults
666
+ ipmg --input targets.txt --profile datacenter # and the profile on top
667
+ ipmg --input targets.txt --threads 20 # a flag always wins
668
+ ```
669
+
670
+ Any long flag can be a key, written as the flag is (`scan-ports`) or with
671
+ underscores (`scan_ports`). A switch takes `true` to mean "as if the flag were
672
+ passed" — `no-history = true` is `--no-history`. Files are read from
673
+ `~/.config/ipmg/config.toml` first (or `$XDG_CONFIG_HOME`), then `./ipmg.toml`
674
+ on top, so a project can override your global defaults:
675
+
676
+ | Flag | What it does |
677
+ | --- | --- |
678
+ | `--config PATH` | Read that file instead of searching |
679
+ | `--no-config` | Ignore every file and use the built-in defaults |
680
+ | `--profile NAME` | Apply the `[profile.NAME]` section as well |
681
+
682
+ **The command line always wins.** A flag that appears on it ignores the file
683
+ entirely for that flag — including `--input`, which merges several sources on
684
+ one command line but replaces the file's list rather than adding to it.
685
+
686
+ A key that is not a flag, a value of the wrong type, or a value outside a
687
+ flag's choices is an error naming the key and the file it came from:
688
+
689
+ ```text
690
+ ✗ Unknown option 'thredas' in ipmg.toml. Did you mean 'threads'?
691
+ ✗ 'formats' in ipmg.toml: 'pdf' is not one of xlsx, csv, json, jsonl, md.
692
+ ```
693
+
694
+ A project's `./ipmg.toml` comes with whatever directory you scan from,
695
+ including a repository you just cloned, so it cannot say where results are
696
+ sent: the `--notify-*` destinations and `--smtp-*` settings are only read from
697
+ `~/.config/ipmg/config.toml` or a file you pass with `--config`. A file also
698
+ cannot combine flags that exclude each other, such as `json` and `jsonl`.
699
+
700
+ ---
701
+
605
702
  ## All options
606
703
 
607
704
  `ipmg --help` always lists the current set. Grouped for reading:
@@ -610,10 +707,12 @@ them — so piping IPMG into a file or a log gives you clean text.
610
707
 
611
708
  | Flag | Default | Description |
612
709
  | --- | --- | --- |
613
- | `--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`) |
710
+ | `--input` | `ip_list.xlsx` | What to scan: one or more files (`.xlsx`, `.xls`, `.csv`, `.json`, `.txt`, `.list`), IPs, CIDR blocks, or ranges (`10.0.0.1-10.0.0.50`), merged and de-duplicated |
614
711
  | `--discover` | off | Auto-detect and scan the local subnet instead |
615
712
  | `--output` | `results` | Report file name prefix |
616
- | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` |
713
+ | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` (no file at all with `--json`/`--jsonl`) |
714
+ | `--json` | off | Print the finished scan to stdout as a JSON array, human output on stderr |
715
+ | `--jsonl` | off | Stream one JSON object per host to stdout as each probe finishes |
617
716
  | `--no-incremental` | off | Only write the report once the scan has finished |
618
717
  | `--autosave` | `30` | How often a running scan re-saves `xlsx`, `json`, and `md` |
619
718
  | `--resume` | off | Finish an interrupted scan from its partial report (newest one, or the path given) |
@@ -645,6 +744,9 @@ them — so piping IPMG into a file or a log gives you clean text.
645
744
  | `--stream-all` | off | Stream every result, including hosts that did not answer (implies `--stream`) |
646
745
  | `--stream-refresh` | `0.25` | Seconds between progress-bar redraws while streaming (0.05-5) |
647
746
  | `--verbose` | off | Debug logging |
747
+ | `--config` | search | Read defaults from this file instead of searching for `ipmg.toml` |
748
+ | `--no-config` | off | Ignore every configuration file |
749
+ | `--profile` | none | Apply a `[profile.NAME]` section from the configuration file |
648
750
 
649
751
  **Exit status**
650
752
 
@@ -283,8 +283,10 @@ Or pick a ready-made command:
283
283
  | --- | --- |
284
284
  | Scan the network I am on | `ipmg --discover` |
285
285
  | Scan hosts listed in a file | `ipmg --input targets.txt` |
286
+ | Scan a file plus a few extra hosts | `ipmg --input targets.txt 10.0.0.0/30 10.0.0.5` |
286
287
  | Get names, not just IP addresses | `ipmg --input targets.txt --resolve` |
287
288
  | Get a report I can send to someone | `ipmg --input targets.txt --formats md csv` |
289
+ | Pipe the results into a script | `ipmg --input targets.txt --json \| jq .` |
288
290
  | See hosts appear as they answer | `ipmg --input 192.168.1.0/24 --stream` |
289
291
  | See what changed since last time | `ipmg --input targets.txt --compare` |
290
292
  | Check which services are listening | `ipmg --input targets.txt --scan-ports` |
@@ -486,9 +488,32 @@ Anywhere IPMG takes `--input`, you can give it any of these:
486
488
  | 192.168.1.1 |
487
489
  | 10.0.1.0/30 |
488
490
 
489
- Duplicate targets are removed automatically, and one scan expands to at most
490
- 65,536 hosts — larger CIDR blocks or ranges are rejected up front, before the
491
- scan starts.
491
+ - **A JSON file** (`.json`) — a list of addresses, a list of objects keyed by
492
+ `IP Address`, `ip`, or `target`, or an object with a `targets` or `ips` array:
493
+
494
+ ```json
495
+ ["192.168.1.1", "10.0.1.0/30"]
496
+ ```
497
+
498
+ Because `IP Address` is one of the keys it accepts, a report IPMG wrote with
499
+ `--formats json` can be fed straight back in:
500
+
501
+ ```bash
502
+ ipmg --input results_20260628_120000.json
503
+ ```
504
+
505
+ `--input` takes as many of these as you like, in any mix, and may also be
506
+ repeated:
507
+
508
+ ```bash
509
+ ipmg --input targets.txt 10.0.0.0/30 10.0.0.5
510
+ ipmg --input targets.txt --input 10.0.0.5 # the same thing
511
+ ```
512
+
513
+ Duplicate targets are removed automatically — across sources too, so a host
514
+ that is both in the file and on the command line is scanned once — and one scan
515
+ expands to at most 65,536 hosts in total. Larger CIDR blocks, ranges, or
516
+ combinations are rejected up front, before the scan starts.
492
517
 
493
518
  ---
494
519
 
@@ -538,6 +563,22 @@ preferring `jsonl` or `csv` (current to the last host) over `json` or `xlsx`
538
563
  (current to the last autosave). Hosts dropped from the target list since are
539
564
  left out of the finished report.
540
565
 
566
+ **Piping results into a script.** `--json` prints the finished scan to stdout as
567
+ a JSON array, and `--jsonl` prints one object per host the moment its probe
568
+ finishes. The human output moves to stderr, so stdout is nothing but data and no
569
+ temp file or glob is needed:
570
+
571
+ ```bash
572
+ ipmg --input 10.0.0.0/24 --json | jq '.[] | select(.Status == "Active")'
573
+ ipmg --input 10.0.0.0/24 --jsonl | while read -r host; do notify "$host"; done
574
+ ipmg --input 10.0.0.0/24 --json 2>/dev/null > hosts.json # data only
575
+ ```
576
+
577
+ The field names are the report columns above, and both flags print a host
578
+ identically — `--json` is what `--jsonl` prints, gathered into an array. Unless
579
+ you ask for `--formats` explicitly, a piped scan writes no report file at all.
580
+ Exit codes are unchanged.
581
+
541
582
  **Open ports.** `Open Ports` is only populated when `--scan-ports` is set: for
542
583
  each host that answers, IPMG probes a list of common TCP ports (SSH, HTTP,
543
584
  HTTPS, RDP, SMB, FTP, SMTP, DNS, MSSQL, MySQL, PostgreSQL by default)
@@ -554,6 +595,61 @@ them — so piping IPMG into a file or a log gives you clean text.
554
595
 
555
596
  ---
556
597
 
598
+ ## Default options in a file
599
+
600
+ If every run repeats the same flags, put them in **`ipmg.toml`** next to your
601
+ work instead:
602
+
603
+ ```toml
604
+ threads = 200
605
+ timeout = 1
606
+ resolve = true
607
+ formats = ["md", "csv"]
608
+
609
+ [profile.datacenter]
610
+ threads = 400
611
+ scan-ports = true
612
+ ports = "22,80,443"
613
+ ```
614
+
615
+ ```bash
616
+ ipmg --input targets.txt # uses the file's defaults
617
+ ipmg --input targets.txt --profile datacenter # and the profile on top
618
+ ipmg --input targets.txt --threads 20 # a flag always wins
619
+ ```
620
+
621
+ Any long flag can be a key, written as the flag is (`scan-ports`) or with
622
+ underscores (`scan_ports`). A switch takes `true` to mean "as if the flag were
623
+ passed" — `no-history = true` is `--no-history`. Files are read from
624
+ `~/.config/ipmg/config.toml` first (or `$XDG_CONFIG_HOME`), then `./ipmg.toml`
625
+ on top, so a project can override your global defaults:
626
+
627
+ | Flag | What it does |
628
+ | --- | --- |
629
+ | `--config PATH` | Read that file instead of searching |
630
+ | `--no-config` | Ignore every file and use the built-in defaults |
631
+ | `--profile NAME` | Apply the `[profile.NAME]` section as well |
632
+
633
+ **The command line always wins.** A flag that appears on it ignores the file
634
+ entirely for that flag — including `--input`, which merges several sources on
635
+ one command line but replaces the file's list rather than adding to it.
636
+
637
+ A key that is not a flag, a value of the wrong type, or a value outside a
638
+ flag's choices is an error naming the key and the file it came from:
639
+
640
+ ```text
641
+ ✗ Unknown option 'thredas' in ipmg.toml. Did you mean 'threads'?
642
+ ✗ 'formats' in ipmg.toml: 'pdf' is not one of xlsx, csv, json, jsonl, md.
643
+ ```
644
+
645
+ A project's `./ipmg.toml` comes with whatever directory you scan from,
646
+ including a repository you just cloned, so it cannot say where results are
647
+ sent: the `--notify-*` destinations and `--smtp-*` settings are only read from
648
+ `~/.config/ipmg/config.toml` or a file you pass with `--config`. A file also
649
+ cannot combine flags that exclude each other, such as `json` and `jsonl`.
650
+
651
+ ---
652
+
557
653
  ## All options
558
654
 
559
655
  `ipmg --help` always lists the current set. Grouped for reading:
@@ -562,10 +658,12 @@ them — so piping IPMG into a file or a log gives you clean text.
562
658
 
563
659
  | Flag | Default | Description |
564
660
  | --- | --- | --- |
565
- | `--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`) |
661
+ | `--input` | `ip_list.xlsx` | What to scan: one or more files (`.xlsx`, `.xls`, `.csv`, `.json`, `.txt`, `.list`), IPs, CIDR blocks, or ranges (`10.0.0.1-10.0.0.50`), merged and de-duplicated |
566
662
  | `--discover` | off | Auto-detect and scan the local subnet instead |
567
663
  | `--output` | `results` | Report file name prefix |
568
- | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` |
664
+ | `--formats` | `xlsx` | One or more of `xlsx`, `csv`, `json`, `jsonl`, `md` (no file at all with `--json`/`--jsonl`) |
665
+ | `--json` | off | Print the finished scan to stdout as a JSON array, human output on stderr |
666
+ | `--jsonl` | off | Stream one JSON object per host to stdout as each probe finishes |
569
667
  | `--no-incremental` | off | Only write the report once the scan has finished |
570
668
  | `--autosave` | `30` | How often a running scan re-saves `xlsx`, `json`, and `md` |
571
669
  | `--resume` | off | Finish an interrupted scan from its partial report (newest one, or the path given) |
@@ -597,6 +695,9 @@ them — so piping IPMG into a file or a log gives you clean text.
597
695
  | `--stream-all` | off | Stream every result, including hosts that did not answer (implies `--stream`) |
598
696
  | `--stream-refresh` | `0.25` | Seconds between progress-bar redraws while streaming (0.05-5) |
599
697
  | `--verbose` | off | Debug logging |
698
+ | `--config` | search | Read defaults from this file instead of searching for `ipmg.toml` |
699
+ | `--no-config` | off | Ignore every configuration file |
700
+ | `--profile` | none | Apply a `[profile.NAME]` section from the configuration file |
600
701
 
601
702
  **Exit status**
602
703
 
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
13
13
 
14
14
  [project]
15
15
  name = "ipmg"
16
- version = "2.3.0" # Managed automatically by semantic-release
16
+ version = "2.4.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"
@@ -57,6 +57,9 @@ dependencies = [
57
57
  "uvicorn>=0.27",
58
58
  "websockets>=12",
59
59
  "python-multipart>=0.0.9",
60
+ # tomllib is in the standard library from 3.11; tomli 2.x is the same
61
+ # parser with the same API, so the floor must not drop below it.
62
+ "tomli>=2.0.1; python_version < '3.11'",
60
63
  ]
61
64
 
62
65
  [project.urls]
@@ -2,4 +2,4 @@
2
2
  ipmg - IP Management & Ping Monitoring Tool
3
3
  """
4
4
 
5
- __version__ = "2.3.0"
5
+ __version__ = "2.4.0"
@@ -6,6 +6,7 @@ import logging
6
6
  import sys
7
7
  from typing import Callable, Dict, List, Optional
8
8
 
9
+ from ipmg.cli import config
9
10
  from ipmg.cli.parser import (
10
11
  build_diff_parser,
11
12
  build_history_parser,
@@ -21,7 +22,7 @@ from ipmg.reporting import ui
21
22
  from ipmg.reporting.diff_report import export_diff, print_diff
22
23
  from ipmg.reporting.summary import print_scan_history
23
24
  from ipmg.services.history_service import HistoryService
24
- from ipmg.services.scan_service import run_scan
25
+ from ipmg.services.scan_service import machine_output, run_scan
25
26
  from ipmg.utils.helpers import configure_logging
26
27
 
27
28
  EXIT_OK = 0
@@ -34,8 +35,16 @@ log = logging.getLogger(__name__)
34
35
 
35
36
 
36
37
  def _scan_command(argv: List[str]) -> int:
37
- args = build_parser().parse_args(argv)
38
+ parser = build_parser()
39
+ # Before parsing: the file supplies the defaults, so anything on the
40
+ # command line still overrides it.
41
+ config.apply(parser, build_parser, argv)
42
+ args = parser.parse_args(argv)
38
43
  configure_logging(args.verbose)
44
+ if machine_output(args).enabled:
45
+ # Before the banner: with --json or --jsonl stdout carries the results
46
+ # and nothing else, so every line a person reads goes to stderr.
47
+ ui.use_stderr()
39
48
  ui.header("scan")
40
49
  print_disclaimer_once()
41
50
  run_scan(args)