netcheckx 2.1.0__py3-none-any.whl
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.
- netcheck/__init__.py +1 -0
- netcheck/__main__.py +14 -0
- netcheck/cli.py +512 -0
- netcheck/mcp/server.py +110 -0
- netcheck/mcp/tools.py +236 -0
- netcheck/modules/dns.py +110 -0
- netcheck/modules/http.py +92 -0
- netcheck/modules/interfaces.py +268 -0
- netcheck/modules/ping.py +113 -0
- netcheck/modules/ssl.py +235 -0
- netcheck/modules/tcp.py +70 -0
- netcheck/utils/cache.py +34 -0
- netcheck/utils/formatters.py +707 -0
- netcheck/utils/normalize.py +87 -0
- netcheck/utils/range_expanders.py +77 -0
- netcheck/utils/retry.py +56 -0
- netcheck/utils/timeout.py +23 -0
- netcheckx-2.1.0.dist-info/METADATA +244 -0
- netcheckx-2.1.0.dist-info/RECORD +23 -0
- netcheckx-2.1.0.dist-info/WHEEL +5 -0
- netcheckx-2.1.0.dist-info/entry_points.txt +2 -0
- netcheckx-2.1.0.dist-info/licenses/LICENSE +34 -0
- netcheckx-2.1.0.dist-info/top_level.txt +1 -0
netcheck/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.0"
|
netcheck/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Ensure parent directory is in sys.path when run directly as `python3 netcheck`
|
|
5
|
+
if __name__ == "__main__" and not __package__:
|
|
6
|
+
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
7
|
+
if parent_dir not in sys.path:
|
|
8
|
+
sys.path.insert(0, parent_dir)
|
|
9
|
+
__package__ = "netcheck"
|
|
10
|
+
|
|
11
|
+
from netcheck.cli import main
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
main()
|
netcheck/cli.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import argparse
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from typing import List, Dict, Any, Tuple, Optional
|
|
10
|
+
|
|
11
|
+
from netcheck.modules.tcp import check_tcp_connect
|
|
12
|
+
from netcheck.modules.dns import dns_lookup
|
|
13
|
+
from netcheck.modules.http import check_http_status
|
|
14
|
+
from netcheck.modules.ssl import check_ssl_certificate
|
|
15
|
+
from netcheck.modules.ping import ping_host
|
|
16
|
+
from netcheck.modules.interfaces import get_network_interfaces
|
|
17
|
+
from netcheck.utils.formatters import format_text, format_json, format_csv, format_xml, get_colors
|
|
18
|
+
from netcheck.utils.range_expanders import expand_ip_range, expand_port_range
|
|
19
|
+
from netcheck.utils.normalize import parse_line_to_raw_host_port
|
|
20
|
+
|
|
21
|
+
def run_check_with_retry(check_fn, args=(), kwargs=None, retries=1, delay=1.0) -> Dict[str, Any]:
|
|
22
|
+
"""Runs a check function and retries it if it fails or returns success=False."""
|
|
23
|
+
if kwargs is None:
|
|
24
|
+
kwargs = {}
|
|
25
|
+
|
|
26
|
+
attempt = 1
|
|
27
|
+
result = None
|
|
28
|
+
while attempt <= retries:
|
|
29
|
+
try:
|
|
30
|
+
result = check_fn(*args, **kwargs)
|
|
31
|
+
if result.get("success", False):
|
|
32
|
+
return result
|
|
33
|
+
except Exception as e:
|
|
34
|
+
result = {
|
|
35
|
+
"target": str(args[0]) if args else "unknown",
|
|
36
|
+
"status": "FAILED",
|
|
37
|
+
"latency_ms": 0.0,
|
|
38
|
+
"success": False,
|
|
39
|
+
"error": str(e),
|
|
40
|
+
"metadata": {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if attempt < retries:
|
|
44
|
+
time.sleep(delay)
|
|
45
|
+
attempt += 1
|
|
46
|
+
|
|
47
|
+
return result or {"success": False, "status": "FAILED", "target": "unknown", "error": "No attempts made"}
|
|
48
|
+
|
|
49
|
+
def parse_csv_content(content: str) -> List[Tuple[str, str]]:
|
|
50
|
+
"""Parses CSV content of hosts and ports."""
|
|
51
|
+
targets = []
|
|
52
|
+
try:
|
|
53
|
+
reader = csv.reader(io.StringIO(content))
|
|
54
|
+
# Skip header if it exists
|
|
55
|
+
first_row = next(reader, None)
|
|
56
|
+
if first_row:
|
|
57
|
+
# Check if first row is header
|
|
58
|
+
if len(first_row) >= 2 and (first_row[0].lower() in ("host", "target", "hostname") or first_row[1].lower() in ("port", "ports")):
|
|
59
|
+
pass
|
|
60
|
+
else:
|
|
61
|
+
targets.append((first_row[0].strip(), first_row[1].strip()))
|
|
62
|
+
|
|
63
|
+
for row in reader:
|
|
64
|
+
if len(row) >= 2:
|
|
65
|
+
targets.append((row[0].strip(), row[1].strip()))
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"Error parsing CSV content: {e}", file=sys.stderr)
|
|
68
|
+
return targets
|
|
69
|
+
|
|
70
|
+
def parse_csv_file(filepath: str) -> List[Tuple[str, str]]:
|
|
71
|
+
"""Parses a CSV file of hosts and ports."""
|
|
72
|
+
try:
|
|
73
|
+
with open(filepath, "r", newline="") as f:
|
|
74
|
+
return parse_csv_content(f.read())
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"Error reading CSV file {filepath}: {e}", file=sys.stderr)
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
def parse_batch_content(content: str) -> List[Tuple[str, str]]:
|
|
80
|
+
"""Parses a lenient batch content containing targets (using parse_line_to_raw_host_port)."""
|
|
81
|
+
targets = []
|
|
82
|
+
for line in content.splitlines():
|
|
83
|
+
h, p = parse_line_to_raw_host_port(line)
|
|
84
|
+
if h and p:
|
|
85
|
+
targets.append((h, p))
|
|
86
|
+
return targets
|
|
87
|
+
|
|
88
|
+
def parse_batch_file(filepath: str) -> List[Tuple[str, str]]:
|
|
89
|
+
"""Parses a lenient batch file of targets."""
|
|
90
|
+
try:
|
|
91
|
+
with open(filepath, "r") as f:
|
|
92
|
+
return parse_batch_content(f.read())
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print(f"Error reading batch file {filepath}: {e}", file=sys.stderr)
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
def print_help():
|
|
98
|
+
cmd_name = "netcheck"
|
|
99
|
+
if len(sys.argv) > 0:
|
|
100
|
+
prog = sys.argv[0]
|
|
101
|
+
if "netcheck" not in prog and ("__main__.py" in prog or "cli.py" in prog):
|
|
102
|
+
cmd_name = "python3 -m netcheck"
|
|
103
|
+
|
|
104
|
+
help_text = f"""Network Connectivity Checker - Advanced Version
|
|
105
|
+
|
|
106
|
+
Usage: {cmd_name} [OPTIONS] [input_file]
|
|
107
|
+
|
|
108
|
+
OPTIONS:
|
|
109
|
+
-t, --timeout <seconds> Connection timeout (default: 5)
|
|
110
|
+
-j, --jobs <number> Max parallel jobs (default: 10)
|
|
111
|
+
-V, --verbose Verbose output
|
|
112
|
+
-f, --format <format> Output format: text, json, csv, xml (default: text)
|
|
113
|
+
-c, --combined Create combined report with all results
|
|
114
|
+
-q, --quick <host> <port> Quick test mode (supports ranges: 80,443 or 8000-8100)
|
|
115
|
+
-o, --output <file> Save quick mode results to file
|
|
116
|
+
-d, --dns <host> Resolve DNS and show IP address (accepts URLs)
|
|
117
|
+
-p, --ping <host> Ping host using ICMP (accepts URLs/IPs)
|
|
118
|
+
-s, --status <url> Check HTTP/HTTPS status code and response time
|
|
119
|
+
--cert <host> Check SSL/TLS certificate validity and expiration
|
|
120
|
+
--my-ip, -ip Show all network interfaces and IP addresses (UP only)
|
|
121
|
+
--my-ip --all Show all interfaces including inactive ones
|
|
122
|
+
--retry <number> Retry failed connections N times (default: 1, no retry)
|
|
123
|
+
--retry-delay <seconds> Delay between retries in seconds (default: 1)
|
|
124
|
+
--csv Input file is in CSV format (host,port)
|
|
125
|
+
-h, --help Show this help message
|
|
126
|
+
-v, --version Show version information
|
|
127
|
+
|
|
128
|
+
INPUT:
|
|
129
|
+
input_file File containing IP:port pairs (one per line)
|
|
130
|
+
If not specified, reads from stdin
|
|
131
|
+
Use --csv flag for CSV format files
|
|
132
|
+
|
|
133
|
+
EXAMPLES:
|
|
134
|
+
{cmd_name} ip-text.txt # Basic usage
|
|
135
|
+
{cmd_name} --csv hosts.csv # Read from CSV file
|
|
136
|
+
{cmd_name} -t 10 -j 20 ip-text.txt # Custom timeout and parallel jobs
|
|
137
|
+
{cmd_name} -f json -c ip-text.txt # JSON output with combined report
|
|
138
|
+
cat ip-text.txt | {cmd_name} -V # Verbose mode from stdin
|
|
139
|
+
{cmd_name} -q 192.168.1.1 80 # Quick test single port
|
|
140
|
+
{cmd_name} -q google.com 80,443 # Quick test multiple ports
|
|
141
|
+
{cmd_name} -q 10.0.0.1-50 22 # Quick test IP range
|
|
142
|
+
{cmd_name} -q 192.168.1.90-95 22 -o results.txt # Save quick mode to file
|
|
143
|
+
{cmd_name} -q 10.0.0.1-100 22 -j 20 # Quick mode with parallel jobs
|
|
144
|
+
{cmd_name} -d google.com # Resolve DNS to IP
|
|
145
|
+
{cmd_name} -d https://api.example.com # DNS from URL (strips scheme/path)
|
|
146
|
+
{cmd_name} -p 8.8.8.8 # Ping Google DNS
|
|
147
|
+
{cmd_name} -p https://github.com # Ping from URL
|
|
148
|
+
{cmd_name} -s https://google.com # Check HTTP status
|
|
149
|
+
{cmd_name} -s api.example.com -V # HTTP status with headers
|
|
150
|
+
{cmd_name} --cert https://google.com # Check SSL certificate
|
|
151
|
+
{cmd_name} --cert github.com:443 -V # Certificate with SANs
|
|
152
|
+
{cmd_name} --my-ip # Show all network interfaces and IPs
|
|
153
|
+
{cmd_name} --my-ip --all # Show all interfaces (including down)
|
|
154
|
+
{cmd_name} --retry 3 --retry-delay 2 hosts.txt # Retry failed connections 3 times with 2s delay
|
|
155
|
+
{cmd_name} -v # Show version
|
|
156
|
+
{cmd_name} -q localhost 8000-8100 # Quick test port range
|
|
157
|
+
echo "192.168.1.1-50 80" | {cmd_name} # Check IP range
|
|
158
|
+
echo "192.168.1.0/24 22" | {cmd_name} # Check CIDR subnet
|
|
159
|
+
echo "host.com 80,443,8080" | {cmd_name} # Check multiple ports
|
|
160
|
+
echo "host.com 8000-8100" | {cmd_name} # Check port range
|
|
161
|
+
|
|
162
|
+
INPUT FORMAT:
|
|
163
|
+
Each line should contain: HOST PORT(S)
|
|
164
|
+
|
|
165
|
+
Basic: 192.168.1.1 80
|
|
166
|
+
IP Range: 192.168.1.1-50 80 (checks .1 through .50)
|
|
167
|
+
CIDR: 192.168.1.0/24 80 (checks entire subnet)
|
|
168
|
+
Multi-port: 192.168.1.1 80,443,8080 (checks multiple ports)
|
|
169
|
+
Port Range: 192.168.1.1 8000-8100 (checks port range)
|
|
170
|
+
Combined: 192.168.1.1-10 80,443 (IP range with multiple ports)
|
|
171
|
+
|
|
172
|
+
CSV FORMAT (with --csv flag):
|
|
173
|
+
host,port
|
|
174
|
+
192.168.1.1,80
|
|
175
|
+
server.com,443
|
|
176
|
+
10.0.0.1-5,22 (ranges supported)
|
|
177
|
+
host.local,"80,443" (multiple ports in quotes)
|
|
178
|
+
"""
|
|
179
|
+
print(help_text)
|
|
180
|
+
|
|
181
|
+
class NetCheckArgumentParser(argparse.ArgumentParser):
|
|
182
|
+
"""Custom parser to output advanced example-rich help on syntax or argument errors."""
|
|
183
|
+
def error(self, message):
|
|
184
|
+
print(f"Error: {message}\n", file=sys.stderr)
|
|
185
|
+
print_help()
|
|
186
|
+
sys.exit(2)
|
|
187
|
+
|
|
188
|
+
def main():
|
|
189
|
+
# Force stdout and stderr to UTF-8 to prevent UnicodeEncodeError on Windows
|
|
190
|
+
try:
|
|
191
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
192
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
193
|
+
except (AttributeError, TypeError):
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
if len(sys.argv) < 2:
|
|
197
|
+
# Check if stdin has data
|
|
198
|
+
if not sys.stdin.isatty():
|
|
199
|
+
lines = sys.stdin.read().splitlines()
|
|
200
|
+
run_batch_lines(lines, timeout=5.0, max_jobs=10, format_name="text", combined=False, retries=1, retry_delay=1.0, verbose=False)
|
|
201
|
+
return
|
|
202
|
+
print_help()
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
|
|
205
|
+
first_arg = sys.argv[1]
|
|
206
|
+
|
|
207
|
+
# 1. Redesigned Subcommand Route
|
|
208
|
+
if first_arg in ("tcp", "dns", "http", "ssl", "ping", "interfaces"):
|
|
209
|
+
handle_subcommands(first_arg, sys.argv[2:])
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
# 2. Legacy Parsing Route
|
|
213
|
+
parser = NetCheckArgumentParser(add_help=False)
|
|
214
|
+
parser.add_argument("-h", "--help", action="store_true")
|
|
215
|
+
parser.add_argument("-v", "--version", action="store_true")
|
|
216
|
+
parser.add_argument("-q", "--quick", nargs=2)
|
|
217
|
+
parser.add_argument("-d", "--dns")
|
|
218
|
+
parser.add_argument("-p", "--ping")
|
|
219
|
+
parser.add_argument("-s", "--status")
|
|
220
|
+
parser.add_argument("--cert")
|
|
221
|
+
parser.add_argument("-ip", "--my-ip", action="store_true")
|
|
222
|
+
parser.add_argument("--mcp", action="store_true")
|
|
223
|
+
parser.add_argument("--csv", action="store_true")
|
|
224
|
+
parser.add_argument("-t", "--timeout", type=float, default=5.0)
|
|
225
|
+
parser.add_argument("-j", "--jobs", type=int, default=10)
|
|
226
|
+
parser.add_argument("-f", "--format", default="text", choices=["text", "json", "csv", "xml"])
|
|
227
|
+
parser.add_argument("-c", "--combined", action="store_true")
|
|
228
|
+
parser.add_argument("-o", "--output")
|
|
229
|
+
parser.add_argument("--retry", type=int, default=1)
|
|
230
|
+
parser.add_argument("--retry-delay", type=float, default=1.0)
|
|
231
|
+
parser.add_argument("-V", "--verbose", action="store_true")
|
|
232
|
+
parser.add_argument("--all", action="store_true")
|
|
233
|
+
parser.add_argument("input_file", nargs="?")
|
|
234
|
+
|
|
235
|
+
args, unknown = parser.parse_known_args()
|
|
236
|
+
|
|
237
|
+
if args.help:
|
|
238
|
+
print_help()
|
|
239
|
+
sys.exit(0)
|
|
240
|
+
|
|
241
|
+
if args.version:
|
|
242
|
+
from netcheck import __version__
|
|
243
|
+
print(f"netcheck version {__version__}")
|
|
244
|
+
sys.exit(0)
|
|
245
|
+
|
|
246
|
+
if args.mcp:
|
|
247
|
+
from netcheck.mcp.server import start_mcp_server
|
|
248
|
+
start_mcp_server()
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Apply format and parameters
|
|
252
|
+
fmt = args.format
|
|
253
|
+
timeout = args.timeout
|
|
254
|
+
retries = args.retry
|
|
255
|
+
retry_delay = args.retry_delay
|
|
256
|
+
verbose = args.verbose
|
|
257
|
+
|
|
258
|
+
if args.my_ip:
|
|
259
|
+
res = get_network_interfaces(all_interfaces=args.all)
|
|
260
|
+
print(format_output([res], fmt, verbose=verbose))
|
|
261
|
+
sys.exit(0 if res["success"] else 1)
|
|
262
|
+
|
|
263
|
+
if args.dns:
|
|
264
|
+
res = run_check_with_retry(dns_lookup, (args.dns, timeout), retries=retries, delay=retry_delay)
|
|
265
|
+
print(format_output([res], fmt, verbose=verbose))
|
|
266
|
+
sys.exit(0 if res["success"] else 1)
|
|
267
|
+
|
|
268
|
+
if args.ping:
|
|
269
|
+
res = run_check_with_retry(ping_host, (args.ping, 4, timeout), retries=retries, delay=retry_delay)
|
|
270
|
+
print(format_output([res], fmt, verbose=verbose))
|
|
271
|
+
sys.exit(0 if res["success"] else 1)
|
|
272
|
+
|
|
273
|
+
if args.status:
|
|
274
|
+
res = run_check_with_retry(check_http_status, (args.status, timeout), retries=retries, delay=retry_delay)
|
|
275
|
+
print(format_output([res], fmt, verbose=verbose))
|
|
276
|
+
sys.exit(0 if res["success"] else 1)
|
|
277
|
+
|
|
278
|
+
if args.cert:
|
|
279
|
+
res = run_check_with_retry(check_ssl_certificate, (args.cert, 443, timeout), retries=retries, delay=retry_delay)
|
|
280
|
+
print(format_output([res], fmt, verbose=verbose))
|
|
281
|
+
sys.exit(0 if res["success"] else 1)
|
|
282
|
+
|
|
283
|
+
if args.quick:
|
|
284
|
+
host, port_str = args.quick
|
|
285
|
+
run_quick_test(host, port_str, timeout, args.jobs, fmt, args.output, retries, retry_delay, verbose=verbose)
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Stdin or File Batch checks
|
|
289
|
+
targets = []
|
|
290
|
+
if args.csv:
|
|
291
|
+
if args.input_file:
|
|
292
|
+
targets = parse_csv_file(args.input_file)
|
|
293
|
+
elif not sys.stdin.isatty():
|
|
294
|
+
targets = parse_csv_content(sys.stdin.read())
|
|
295
|
+
else:
|
|
296
|
+
print("Error: No CSV input file or stdin stream provided", file=sys.stderr)
|
|
297
|
+
sys.exit(1)
|
|
298
|
+
run_batch_targets(targets, timeout, args.jobs, fmt, args.combined, retries, retry_delay, verbose=verbose)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
if args.input_file:
|
|
302
|
+
targets = parse_batch_file(args.input_file)
|
|
303
|
+
run_batch_targets(targets, timeout, args.jobs, fmt, args.combined, retries, retry_delay, verbose=verbose)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
# Stdin fallback if no args are matched
|
|
307
|
+
if not sys.stdin.isatty():
|
|
308
|
+
targets = parse_batch_content(sys.stdin.read())
|
|
309
|
+
run_batch_targets(targets, timeout, args.jobs, fmt, args.combined, retries, retry_delay, verbose=verbose)
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
print_help()
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
|
|
315
|
+
def handle_subcommands(subcommand: str, sub_args: List[str]):
|
|
316
|
+
parser = argparse.ArgumentParser(prog=f"netcheck {subcommand}")
|
|
317
|
+
parser.add_argument("-t", "--timeout", type=float, default=5.0)
|
|
318
|
+
parser.add_argument("-f", "--format", default="text", choices=["text", "json", "csv", "xml"])
|
|
319
|
+
parser.add_argument("--retry", type=int, default=1)
|
|
320
|
+
parser.add_argument("--retry-delay", type=float, default=1.0)
|
|
321
|
+
parser.add_argument("-V", "--verbose", action="store_true")
|
|
322
|
+
|
|
323
|
+
if subcommand == "tcp":
|
|
324
|
+
parser.add_argument("host")
|
|
325
|
+
parser.add_argument("port")
|
|
326
|
+
parser.add_argument("-j", "--jobs", type=int, default=10)
|
|
327
|
+
parser.add_argument("-o", "--output")
|
|
328
|
+
args = parser.parse_args(sub_args)
|
|
329
|
+
run_quick_test(args.host, args.port, args.timeout, args.jobs, args.format, args.output, args.retry, args.retry_delay, verbose=args.verbose)
|
|
330
|
+
|
|
331
|
+
elif subcommand == "dns":
|
|
332
|
+
parser.add_argument("host")
|
|
333
|
+
args = parser.parse_args(sub_args)
|
|
334
|
+
res = run_check_with_retry(dns_lookup, (args.host, args.timeout), retries=args.retry, delay=args.retry_delay)
|
|
335
|
+
print(format_output([res], args.format, verbose=args.verbose))
|
|
336
|
+
sys.exit(0 if res["success"] else 1)
|
|
337
|
+
|
|
338
|
+
elif subcommand == "http":
|
|
339
|
+
parser.add_argument("url")
|
|
340
|
+
args = parser.parse_args(sub_args)
|
|
341
|
+
res = run_check_with_retry(check_http_status, (args.url, args.timeout), retries=args.retry, delay=args.retry_delay)
|
|
342
|
+
print(format_output([res], args.format, verbose=args.verbose))
|
|
343
|
+
sys.exit(0 if res["success"] else 1)
|
|
344
|
+
|
|
345
|
+
elif subcommand == "ssl":
|
|
346
|
+
parser.add_argument("host")
|
|
347
|
+
parser.add_argument("port", type=int, nargs="?", default=443)
|
|
348
|
+
args = parser.parse_args(sub_args)
|
|
349
|
+
res = run_check_with_retry(check_ssl_certificate, (args.host, args.port, args.timeout), retries=args.retry, delay=args.retry_delay)
|
|
350
|
+
print(format_output([res], args.format, verbose=args.verbose))
|
|
351
|
+
sys.exit(0 if res["success"] else 1)
|
|
352
|
+
|
|
353
|
+
elif subcommand == "ping":
|
|
354
|
+
parser.add_argument("host")
|
|
355
|
+
parser.add_argument("-c", "--count", type=int, default=4)
|
|
356
|
+
args = parser.parse_args(sub_args)
|
|
357
|
+
res = run_check_with_retry(ping_host, (args.host, args.count, args.timeout), retries=args.retry, delay=args.retry_delay)
|
|
358
|
+
print(format_output([res], args.format, verbose=args.verbose))
|
|
359
|
+
sys.exit(0 if res["success"] else 1)
|
|
360
|
+
|
|
361
|
+
elif subcommand == "interfaces":
|
|
362
|
+
parser.add_argument("--all", action="store_true")
|
|
363
|
+
args = parser.parse_args(sub_args)
|
|
364
|
+
res = get_network_interfaces(all_interfaces=args.all)
|
|
365
|
+
print(format_output([res], args.format, verbose=args.verbose))
|
|
366
|
+
sys.exit(0 if res["success"] else 1)
|
|
367
|
+
|
|
368
|
+
def run_quick_test(host: str, port_str: str, timeout: float, max_jobs: int, fmt: str, output_file: str, retries: int, retry_delay: float, verbose: bool = False):
|
|
369
|
+
hosts = expand_ip_range(host)
|
|
370
|
+
ports = expand_port_range(port_str)
|
|
371
|
+
|
|
372
|
+
targets = []
|
|
373
|
+
for h in hosts:
|
|
374
|
+
for p in ports:
|
|
375
|
+
targets.append((h, p))
|
|
376
|
+
|
|
377
|
+
if not targets:
|
|
378
|
+
print("Error: No valid host or port specified", file=sys.stderr)
|
|
379
|
+
sys.exit(1)
|
|
380
|
+
|
|
381
|
+
results = execute_concurrent_checks(targets, timeout, max_jobs, retries, retry_delay, verbose=verbose)
|
|
382
|
+
|
|
383
|
+
output_str = format_output(results, fmt, verbose=verbose)
|
|
384
|
+
print(output_str)
|
|
385
|
+
|
|
386
|
+
if output_file:
|
|
387
|
+
try:
|
|
388
|
+
with open(output_file, "w") as f:
|
|
389
|
+
f.write(format_output(results, fmt, verbose=verbose, use_color=False))
|
|
390
|
+
print(f"Results saved to: {output_file}")
|
|
391
|
+
except Exception as e:
|
|
392
|
+
print(f"Error saving results to file {output_file}: {e}", file=sys.stderr)
|
|
393
|
+
|
|
394
|
+
all_success = all(r["success"] for r in results)
|
|
395
|
+
sys.exit(0 if all_success else 1)
|
|
396
|
+
|
|
397
|
+
def run_batch_targets(targets: List[Tuple[str, str]], timeout: float, max_jobs: int, fmt: str, combined: bool, retries: int, retry_delay: float, verbose: bool = False):
|
|
398
|
+
expanded_targets = []
|
|
399
|
+
for host, p_str in targets:
|
|
400
|
+
ports = expand_port_range(p_str)
|
|
401
|
+
hosts = expand_ip_range(host)
|
|
402
|
+
for h in hosts:
|
|
403
|
+
for p in ports:
|
|
404
|
+
expanded_targets.append((h, p))
|
|
405
|
+
|
|
406
|
+
if not expanded_targets:
|
|
407
|
+
print("Error: No targets found to test", file=sys.stderr)
|
|
408
|
+
sys.exit(1)
|
|
409
|
+
|
|
410
|
+
results = execute_concurrent_checks(expanded_targets, timeout, max_jobs, retries, retry_delay, verbose=verbose)
|
|
411
|
+
|
|
412
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
413
|
+
ext = "json" if fmt == "json" else "csv" if fmt == "csv" else "xml" if fmt == "xml" else "txt"
|
|
414
|
+
|
|
415
|
+
success_results = [r for r in results if r["success"]]
|
|
416
|
+
fail_results = [r for r in results if not r["success"]]
|
|
417
|
+
|
|
418
|
+
res_filename = f"result-{date_str}.{ext}"
|
|
419
|
+
fail_filename = f"fail-{date_str}.{ext}"
|
|
420
|
+
comb_filename = f"combined-{date_str}.{ext}"
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
if success_results:
|
|
424
|
+
with open(res_filename, "w") as f:
|
|
425
|
+
f.write(format_output(success_results, fmt, verbose=verbose, use_color=False))
|
|
426
|
+
if fail_results:
|
|
427
|
+
with open(fail_filename, "w") as f:
|
|
428
|
+
f.write(format_output(fail_results, fmt, verbose=verbose, use_color=False))
|
|
429
|
+
if combined:
|
|
430
|
+
with open(comb_filename, "w") as f:
|
|
431
|
+
f.write(format_output(results, fmt, verbose=verbose, use_color=False))
|
|
432
|
+
|
|
433
|
+
print(f"Check Complete! Results written to output files.")
|
|
434
|
+
print(f"Successful checks written to: {res_filename} ({len(success_results)} items)")
|
|
435
|
+
print(f"Failed checks written to: {fail_filename} ({len(fail_results)} items)")
|
|
436
|
+
if combined:
|
|
437
|
+
print(f"Combined report written to: {comb_filename}")
|
|
438
|
+
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print(f"Error saving batch output files: {e}", file=sys.stderr)
|
|
441
|
+
|
|
442
|
+
print(format_output(results, fmt, verbose=verbose))
|
|
443
|
+
sys.exit(0 if len(fail_results) == 0 else 1)
|
|
444
|
+
|
|
445
|
+
def run_batch_lines(lines: List[str], timeout: float, max_jobs: int, format_name: str, combined: bool, retries: int, retry_delay: float, verbose: bool = False):
|
|
446
|
+
content = "\n".join(lines)
|
|
447
|
+
targets = parse_batch_content(content)
|
|
448
|
+
run_batch_targets(targets, timeout, max_jobs, format_name, combined, retries, retry_delay, verbose=verbose)
|
|
449
|
+
|
|
450
|
+
def execute_concurrent_checks(targets: List[Tuple[str, int]], timeout: float, max_jobs: int, retries: int, retry_delay: float, verbose: bool = False) -> List[Dict[str, Any]]:
|
|
451
|
+
results = []
|
|
452
|
+
|
|
453
|
+
with ThreadPoolExecutor(max_workers=max_jobs) as executor:
|
|
454
|
+
futures = {}
|
|
455
|
+
for host, port in targets:
|
|
456
|
+
fut = executor.submit(
|
|
457
|
+
run_check_with_retry,
|
|
458
|
+
check_tcp_connect,
|
|
459
|
+
args=(host, int(port), timeout),
|
|
460
|
+
retries=retries,
|
|
461
|
+
delay=retry_delay
|
|
462
|
+
)
|
|
463
|
+
futures[fut] = (host, port)
|
|
464
|
+
|
|
465
|
+
completed = 0
|
|
466
|
+
total = len(targets)
|
|
467
|
+
|
|
468
|
+
for fut in as_completed(futures):
|
|
469
|
+
host, port = futures[fut]
|
|
470
|
+
try:
|
|
471
|
+
res = fut.result()
|
|
472
|
+
results.append(res)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
res = {
|
|
475
|
+
"target": f"{host}:{port}",
|
|
476
|
+
"status": "FAILED",
|
|
477
|
+
"latency_ms": 0.0,
|
|
478
|
+
"success": False,
|
|
479
|
+
"error": str(e),
|
|
480
|
+
"metadata": {"host": host, "port": port}
|
|
481
|
+
}
|
|
482
|
+
results.append(res)
|
|
483
|
+
|
|
484
|
+
completed += 1
|
|
485
|
+
|
|
486
|
+
# Print real-time connection status if verbose is enabled
|
|
487
|
+
if verbose:
|
|
488
|
+
use_color = sys.stdout.isatty()
|
|
489
|
+
c_ansi = get_colors(use_color)
|
|
490
|
+
if res.get("success", False):
|
|
491
|
+
sys.stderr.write(f"{c_ansi['green']}✓ SUCCESS:{c_ansi['reset']} {host}:{port} ({res.get('latency_ms', '?')}ms)\n")
|
|
492
|
+
else:
|
|
493
|
+
sys.stderr.write(f"{c_ansi['red']}✗ FAILED:{c_ansi['reset']} {host}:{port} ({res.get('error', 'unknown error')})\n")
|
|
494
|
+
sys.stderr.flush()
|
|
495
|
+
elif total > 5 and sys.stdout.isatty():
|
|
496
|
+
sys.stdout.write(f"\rProgress: {completed}/{total} completed ({int(completed/total * 100)}%)...")
|
|
497
|
+
sys.stdout.flush()
|
|
498
|
+
|
|
499
|
+
if total > 5 and sys.stdout.isatty() and not verbose:
|
|
500
|
+
print("")
|
|
501
|
+
|
|
502
|
+
return results
|
|
503
|
+
|
|
504
|
+
def format_output(results: List[Dict[str, Any]], format_name: str, verbose: bool = False, use_color: Optional[bool] = None) -> str:
|
|
505
|
+
if format_name == "json":
|
|
506
|
+
return format_json(results)
|
|
507
|
+
elif format_name == "csv":
|
|
508
|
+
return format_csv(results)
|
|
509
|
+
elif format_name == "xml":
|
|
510
|
+
return format_xml(results)
|
|
511
|
+
else:
|
|
512
|
+
return format_text(results, verbose=verbose, use_color=use_color)
|
netcheck/mcp/server.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
from netcheck.mcp.tools import TOOLS_LIST, call_tool
|
|
4
|
+
|
|
5
|
+
def start_mcp_server():
|
|
6
|
+
"""
|
|
7
|
+
Starts the MCP Server listening for JSON-RPC 2.0 messages on stdin
|
|
8
|
+
and writing responses to stdout. Redirects other standard outputs to stderr.
|
|
9
|
+
"""
|
|
10
|
+
# Configure stdin and stdout to use UTF-8 and line-buffering
|
|
11
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
12
|
+
sys.stdin.reconfigure(encoding='utf-8')
|
|
13
|
+
|
|
14
|
+
# Save original stdout for JSON-RPC messages
|
|
15
|
+
original_stdout = sys.stdout
|
|
16
|
+
|
|
17
|
+
# Redirect all standard prints to sys.stderr to prevent corrupting the JSON-RPC stream
|
|
18
|
+
sys.stdout = sys.stderr
|
|
19
|
+
|
|
20
|
+
for line in sys.stdin:
|
|
21
|
+
line = line.strip()
|
|
22
|
+
if not line:
|
|
23
|
+
continue
|
|
24
|
+
try:
|
|
25
|
+
request = json.loads(line)
|
|
26
|
+
method = request.get("method")
|
|
27
|
+
req_id = request.get("id")
|
|
28
|
+
|
|
29
|
+
if method == "initialize":
|
|
30
|
+
response = {
|
|
31
|
+
"jsonrpc": "2.0",
|
|
32
|
+
"id": req_id,
|
|
33
|
+
"result": {
|
|
34
|
+
"protocolVersion": "2024-11-05",
|
|
35
|
+
"capabilities": {
|
|
36
|
+
"tools": {}
|
|
37
|
+
},
|
|
38
|
+
"serverInfo": {
|
|
39
|
+
"name": "netcheck",
|
|
40
|
+
"version": "2.1.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
elif method == "notifications/initialized":
|
|
45
|
+
continue
|
|
46
|
+
elif method == "tools/list":
|
|
47
|
+
response = {
|
|
48
|
+
"jsonrpc": "2.0",
|
|
49
|
+
"id": req_id,
|
|
50
|
+
"result": {
|
|
51
|
+
"tools": TOOLS_LIST
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
elif method == "tools/call":
|
|
55
|
+
params = request.get("params", {})
|
|
56
|
+
tool_name = params.get("name")
|
|
57
|
+
tool_args = params.get("arguments", {})
|
|
58
|
+
|
|
59
|
+
tool_res = call_tool(tool_name, tool_args)
|
|
60
|
+
|
|
61
|
+
response = {
|
|
62
|
+
"jsonrpc": "2.0",
|
|
63
|
+
"id": req_id,
|
|
64
|
+
"result": tool_res
|
|
65
|
+
}
|
|
66
|
+
elif method == "ping":
|
|
67
|
+
response = {
|
|
68
|
+
"jsonrpc": "2.0",
|
|
69
|
+
"id": req_id,
|
|
70
|
+
"result": {}
|
|
71
|
+
}
|
|
72
|
+
else:
|
|
73
|
+
response = {
|
|
74
|
+
"jsonrpc": "2.0",
|
|
75
|
+
"id": req_id,
|
|
76
|
+
"error": {
|
|
77
|
+
"code": -32601,
|
|
78
|
+
"message": f"Method not found: {method}"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
original_stdout.write(json.dumps(response) + "\n")
|
|
83
|
+
original_stdout.flush()
|
|
84
|
+
|
|
85
|
+
except json.JSONDecodeError:
|
|
86
|
+
response = {
|
|
87
|
+
"jsonrpc": "2.0",
|
|
88
|
+
"id": None,
|
|
89
|
+
"error": {
|
|
90
|
+
"code": -32700,
|
|
91
|
+
"message": "Parse error"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
original_stdout.write(json.dumps(response) + "\n")
|
|
95
|
+
original_stdout.flush()
|
|
96
|
+
except Exception as e:
|
|
97
|
+
req_id = locals().get("req_id", None)
|
|
98
|
+
response = {
|
|
99
|
+
"jsonrpc": "2.0",
|
|
100
|
+
"id": req_id,
|
|
101
|
+
"error": {
|
|
102
|
+
"code": -32603,
|
|
103
|
+
"message": f"Internal error: {str(e)}"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
original_stdout.write(json.dumps(response) + "\n")
|
|
107
|
+
original_stdout.flush()
|
|
108
|
+
|
|
109
|
+
# Restore stdout upon exit
|
|
110
|
+
sys.stdout = original_stdout
|