mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-10 04:00:44 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
|
|
@ -1,40 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
|
||||
|
||||
def config_diff_tool() -> ToolSpec:
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
left_name = str(args.get("left_name") or "left")
|
||||
right_name = str(args.get("right_name") or "right")
|
||||
left = str(args.get("left") or "")
|
||||
right = str(args.get("right") or "")
|
||||
left_lines = left.splitlines(keepends=False)
|
||||
right_lines = right.splitlines(keepends=False)
|
||||
diff_lines = list(
|
||||
difflib.unified_diff(left_lines, right_lines, fromfile=left_name, tofile=right_name, lineterm="")
|
||||
)
|
||||
return {"ok": True, "diff": "\n".join(diff_lines), "changed": left_lines != right_lines}
|
||||
|
||||
return ToolSpec(
|
||||
name="config_diff",
|
||||
description="Compare two configuration texts and return a unified diff.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"left_name": {"type": "string", "description": "Optional label for the left side."},
|
||||
"right_name": {"type": "string", "description": "Optional label for the right side."},
|
||||
"left": {"type": "string", "description": "Left configuration text."},
|
||||
"right": {"type": "string", "description": "Right configuration text."},
|
||||
},
|
||||
"required": ["left", "right"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["config_diff_tool"]
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
|
||||
_TTL_RE = re.compile(r"\bttl[= ]\d+\b", re.IGNORECASE)
|
||||
_AVG_WIN_RE = re.compile(r"Average\s*=\s*(\d+)\s*ms", re.IGNORECASE)
|
||||
_AVG_NIX_RE = re.compile(r"=\s*[\d.]+/([\d.]+)/[\d.]+/[\d.]+\s*ms")
|
||||
|
||||
|
||||
def _ping(host: str, count: int, timeout_ms: int) -> dict[str, Any]:
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
cmd = ["ping", "-n", str(count), "-w", str(timeout_ms), host]
|
||||
timeout_s = max(1, (timeout_ms * count) / 1000 + 2)
|
||||
else:
|
||||
timeout_s_each = max(1, int(round(timeout_ms / 1000)))
|
||||
cmd = ["ping", "-c", str(count), "-W", str(timeout_s_each), host]
|
||||
timeout_s = max(1, timeout_s_each * count + 2)
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_s)
|
||||
except FileNotFoundError:
|
||||
return {"ok": False, "error": "ping command not found on this system"}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"ok": True, "reachable": False, "output": "ping timed out"}
|
||||
|
||||
output = (proc.stdout or "") + ("\n" + proc.stderr if proc.stderr else "")
|
||||
reachable = proc.returncode == 0 and bool(_TTL_RE.search(output))
|
||||
avg_ms = None
|
||||
if sys.platform == "win32":
|
||||
m = _AVG_WIN_RE.search(output)
|
||||
if m:
|
||||
try:
|
||||
avg_ms = int(m.group(1))
|
||||
except ValueError:
|
||||
avg_ms = None
|
||||
else:
|
||||
m2 = _AVG_NIX_RE.search(output)
|
||||
if m2:
|
||||
try:
|
||||
avg_ms = int(float(m2.group(1)))
|
||||
except ValueError:
|
||||
avg_ms = None
|
||||
return {"ok": True, "reachable": reachable, "avg_ms": avg_ms, "returncode": proc.returncode, "output": output}
|
||||
|
||||
|
||||
def device_status_tool() -> ToolSpec:
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
host = str(args.get("host"))
|
||||
count = int(args.get("count") or 2)
|
||||
timeout_ms = int(args.get("timeout_ms") or 1000)
|
||||
if count < 1 or count > 10:
|
||||
return {"ok": False, "error": "count must be between 1 and 10"}
|
||||
if timeout_ms < 200 or timeout_ms > 10000:
|
||||
return {"ok": False, "error": "timeout_ms must be between 200 and 10000"}
|
||||
return _ping(host=host, count=count, timeout_ms=timeout_ms)
|
||||
|
||||
return ToolSpec(
|
||||
name="device_status",
|
||||
description="Check host reachability using ICMP ping (system ping binary).",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {"type": "string", "description": "Hostname or IP address."},
|
||||
"count": {"type": "integer", "description": "Number of ping probes. Default 2."},
|
||||
"timeout_ms": {"type": "integer", "description": "Per-packet timeout in milliseconds. Default 1000."},
|
||||
},
|
||||
"required": ["host"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["device_status_tool"]
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
|
||||
|
||||
def _default_edges() -> list[tuple[str, str]]:
|
||||
return [
|
||||
("R1", "R2"),
|
||||
("R2", "R3"),
|
||||
("R3", "R4"),
|
||||
("R2", "R5"),
|
||||
("R5", "R4"),
|
||||
("R1", "SW1"),
|
||||
("SW1", "FW1"),
|
||||
("FW1", "R3"),
|
||||
]
|
||||
|
||||
|
||||
def _build_adj(edges: list[tuple[str, str]]) -> dict[str, set[str]]:
|
||||
adj: dict[str, set[str]] = {}
|
||||
for a, b in edges:
|
||||
adj.setdefault(a, set()).add(b)
|
||||
adj.setdefault(b, set()).add(a)
|
||||
return adj
|
||||
|
||||
|
||||
def _bfs_path(adj: dict[str, set[str]], src: str, dst: str) -> list[str] | None:
|
||||
if src == dst:
|
||||
return [src]
|
||||
q: deque[str] = deque([src])
|
||||
prev: dict[str, str | None] = {src: None}
|
||||
while q:
|
||||
cur = q.popleft()
|
||||
for nxt in sorted(adj.get(cur, set())):
|
||||
if nxt in prev:
|
||||
continue
|
||||
prev[nxt] = cur
|
||||
if nxt == dst:
|
||||
q.clear()
|
||||
break
|
||||
q.append(nxt)
|
||||
if dst not in prev:
|
||||
return None
|
||||
path: list[str] = []
|
||||
cur2: str | None = dst
|
||||
while cur2 is not None:
|
||||
path.append(cur2)
|
||||
cur2 = prev[cur2]
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
|
||||
def get_path_tool() -> ToolSpec:
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
src = str(args.get("src"))
|
||||
dst = str(args.get("dst"))
|
||||
raw_edges = args.get("topology_edges")
|
||||
|
||||
edges: list[tuple[str, str]]
|
||||
if raw_edges is None:
|
||||
edges = _default_edges()
|
||||
else:
|
||||
edges = []
|
||||
for item in raw_edges:
|
||||
if not isinstance(item, (list, tuple)) or len(item) != 2:
|
||||
return {"ok": False, "error": "topology_edges must be an array of pairs; each item must contain two node names."}
|
||||
edges.append((str(item[0]), str(item[1])))
|
||||
|
||||
adj = _build_adj(edges)
|
||||
path = _bfs_path(adj, src, dst)
|
||||
if not path:
|
||||
return {"ok": False, "src": src, "dst": dst, "error": "No reachable path in the given topology."}
|
||||
return {"ok": True, "src": src, "dst": dst, "hops": path, "hop_count": len(path) - 1}
|
||||
|
||||
return ToolSpec(
|
||||
name="get_path",
|
||||
description="Compute the shortest path from src to dst (BFS) over an undirected topology. Optional topology_edges overrides the built-in demo graph.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"src": {"type": "string", "description": "Source node name."},
|
||||
"dst": {"type": "string", "description": "Destination node name."},
|
||||
"topology_edges": {
|
||||
"type": "array",
|
||||
"description": 'Optional edge list, e.g. [["R1","R2"],["R2","R3"]].',
|
||||
"items": {"type": "array", "items": {"type": "string"}, "minItems": 2, "maxItems": 2},
|
||||
},
|
||||
},
|
||||
"required": ["src", "dst"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["get_path_tool"]
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
|
||||
_LEVEL_RE = re.compile(r"\b(ERROR|WARN|WARNING|INFO|DEBUG)\b", re.IGNORECASE)
|
||||
|
||||
|
||||
def log_analysis_tool() -> ToolSpec:
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
text = str(args.get("log") or "")
|
||||
max_lines = int(args.get("max_lines") or 2000)
|
||||
lines = text.splitlines()
|
||||
if len(lines) > max_lines:
|
||||
lines = lines[-max_lines:]
|
||||
|
||||
levels: Counter[str] = Counter()
|
||||
samples: dict[str, list[str]] = {"ERROR": [], "WARN": []}
|
||||
for line in lines:
|
||||
m = _LEVEL_RE.search(line)
|
||||
if not m:
|
||||
continue
|
||||
level = m.group(1).upper()
|
||||
if level == "WARNING":
|
||||
level = "WARN"
|
||||
if level in ("ERROR", "WARN", "INFO", "DEBUG"):
|
||||
levels[level] += 1
|
||||
if level in samples and len(samples[level]) < 5:
|
||||
samples[level].append(line[:500])
|
||||
|
||||
top_lines = [l[:500] for l in lines[-20:]]
|
||||
return {"ok": True, "line_count": len(lines), "level_count": dict(levels), "samples": samples, "tail": top_lines}
|
||||
|
||||
return ToolSpec(
|
||||
name="log_analysis",
|
||||
description="Summarize log text: counts of ERROR/WARN/INFO/DEBUG lines, sample lines, and the last lines (tail).",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"log": {"type": "string", "description": "Log text to analyze."},
|
||||
"max_lines": {"type": "integer", "description": "Maximum number of lines to analyze (uses the tail if exceeded). Default 2000."},
|
||||
},
|
||||
"required": ["log"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["log_analysis_tool"]
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import datetime
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
|
||||
|
||||
def dns_lookup_tool() -> ToolSpec:
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
domain = args.get("domain")
|
||||
if not domain:
|
||||
return {"ok": False, "error": "domain is required"}
|
||||
try:
|
||||
ips = socket.gethostbyname_ex(domain)[2]
|
||||
return {"ok": True, "domain": domain, "ips": ips, "count": len(ips)}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": f"DNS resolution failed: {e}"}
|
||||
|
||||
return ToolSpec(
|
||||
name="dns_lookup",
|
||||
description="Resolve a domain name to IPv4 addresses (A records via system resolver).",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {"type": "string", "description": "Domain name (e.g. example.com)."},
|
||||
},
|
||||
"required": ["domain"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
def ssl_check_tool() -> ToolSpec:
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
domain = args.get("domain")
|
||||
port = int(args.get("port") or 443)
|
||||
if not domain:
|
||||
return {"ok": False, "error": "domain is required"}
|
||||
try:
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((domain, port), timeout=10) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=domain) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
not_before = datetime.datetime.strptime(cert["notBefore"], "%b %d %H:%M:%S %Y %Z")
|
||||
not_after = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
|
||||
remaining_days = (not_after - datetime.datetime.utcnow()).days
|
||||
subject = dict(x[0] for x in cert["subject"])
|
||||
issuer = dict(x[0] for x in cert["issuer"])
|
||||
return {
|
||||
"ok": True,
|
||||
"domain": domain,
|
||||
"issuer": issuer.get("commonName"),
|
||||
"issued_to": subject.get("commonName"),
|
||||
"valid_from": not_before.strftime("%Y-%m-%d"),
|
||||
"valid_until": not_after.strftime("%Y-%m-%d"),
|
||||
"remaining_days": remaining_days,
|
||||
"is_expired": remaining_days < 0,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": f"SSL check failed: {e}"}
|
||||
|
||||
return ToolSpec(
|
||||
name="ssl_cert_check",
|
||||
description="Inspect the TLS certificate presented by host:port (default 443).",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {"type": "string", "description": "Server hostname."},
|
||||
"port": {"type": "integer", "description": "TCP port. Default 443."},
|
||||
},
|
||||
"required": ["domain"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
def port_check_tool() -> ToolSpec:
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
host = args.get("host")
|
||||
port = int(args.get("port"))
|
||||
protocol = str(args.get("protocol") or "tcp").lower()
|
||||
timeout = float(args.get("timeout") or 2.0)
|
||||
if not host or not port:
|
||||
return {"ok": False, "error": "host and port are required"}
|
||||
if protocol == "tcp":
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
return {"ok": True, "host": host, "port": port, "protocol": "TCP", "status": "open"}
|
||||
except socket.timeout:
|
||||
return {"ok": True, "host": host, "port": port, "protocol": "TCP", "status": "timeout"}
|
||||
except Exception as e:
|
||||
return {"ok": True, "host": host, "port": port, "protocol": "TCP", "status": "closed", "error": str(e)}
|
||||
if protocol == "udp":
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(timeout)
|
||||
sock.sendto(b"", (host, port))
|
||||
try:
|
||||
sock.recvfrom(1024)
|
||||
return {"ok": True, "host": host, "port": port, "protocol": "UDP", "status": "open", "received": True}
|
||||
except socket.timeout:
|
||||
return {"ok": True, "host": host, "port": port, "protocol": "UDP", "status": "open|filtered"}
|
||||
except Exception as e:
|
||||
return {"ok": True, "host": host, "port": port, "protocol": "UDP", "status": "closed", "error": str(e)}
|
||||
finally:
|
||||
sock.close()
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": f"UDP check failed: {e}"}
|
||||
return {"ok": False, "error": f"Unsupported protocol: {protocol}"}
|
||||
|
||||
return ToolSpec(
|
||||
name="port_check",
|
||||
description="Test whether a TCP or UDP port appears open on a host.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {"type": "string", "description": "Hostname or IP address."},
|
||||
"port": {"type": "integer", "description": "Port number."},
|
||||
"protocol": {"type": "string", "enum": ["tcp", "udp"], "description": "tcp or udp. Default tcp."},
|
||||
"timeout": {"type": "number", "description": "Timeout in seconds. Default 2.0."},
|
||||
},
|
||||
"required": ["host", "port"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
def port_scan_tool() -> ToolSpec:
|
||||
COMMON_PORTS = [21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 1433, 1521, 3306, 3389, 5432, 6379, 8080, 27017]
|
||||
|
||||
def scan_port(host: str, port: int, timeout: float) -> int | None:
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
return port
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
host = args.get("host")
|
||||
start_port = args.get("start_port")
|
||||
end_port = args.get("end_port")
|
||||
ports_to_scan = args.get("ports")
|
||||
timeout = float(args.get("timeout") or 0.5)
|
||||
max_threads = int(args.get("max_threads") or 20)
|
||||
if not host:
|
||||
return {"ok": False, "error": "host is required"}
|
||||
if ports_to_scan:
|
||||
ports = [int(p) for p in ports_to_scan]
|
||||
elif start_port is not None and end_port is not None:
|
||||
s, e = int(start_port), int(end_port)
|
||||
if e - s > 1000:
|
||||
return {"ok": False, "error": "Cannot scan more than 1000 ports in one call"}
|
||||
ports = list(range(s, e + 1))
|
||||
else:
|
||||
ports = COMMON_PORTS
|
||||
open_ports: list[int] = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_threads) as executor:
|
||||
future_to_port = {executor.submit(scan_port, host, port, timeout): port for port in ports}
|
||||
for future in concurrent.futures.as_completed(future_to_port):
|
||||
result = future.result()
|
||||
if result is not None:
|
||||
open_ports.append(result)
|
||||
open_ports.sort()
|
||||
return {
|
||||
"ok": True,
|
||||
"host": host,
|
||||
"open_ports": open_ports,
|
||||
"scanned_count": len(ports),
|
||||
"open_count": len(open_ports),
|
||||
"status": "completed",
|
||||
}
|
||||
|
||||
return ToolSpec(
|
||||
name="port_scan",
|
||||
description="Scan TCP ports on a host (common ports, a numeric range, or an explicit list).",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {"type": "string", "description": "Hostname or IP address."},
|
||||
"start_port": {"type": "integer", "description": "Start of port range (inclusive)."},
|
||||
"end_port": {"type": "integer", "description": "End of port range (inclusive)."},
|
||||
"ports": {"type": "array", "items": {"type": "integer"}, "description": "Explicit list of ports to scan."},
|
||||
"timeout": {"type": "number", "description": "Per-port timeout in seconds. Default 0.5."},
|
||||
"max_threads": {"type": "integer", "description": "Maximum concurrent probes. Default 20."},
|
||||
},
|
||||
"required": ["host"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
def local_net_info_tool() -> ToolSpec:
|
||||
def get_mac_address() -> str:
|
||||
return ":".join(["{:02x}".format((uuid.getnode() >> i) & 0xFF) for i in range(0, 8 * 6, 8)][::-1])
|
||||
|
||||
def get_public_ip() -> str:
|
||||
try:
|
||||
with httpx.Client(timeout=5.0) as client:
|
||||
resp = client.get("https://api64.ipify.org?format=json")
|
||||
return str(resp.json().get("ip") or "Unknown")
|
||||
except Exception:
|
||||
return "Unknown"
|
||||
|
||||
def get_gateway() -> str:
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
output = subprocess.check_output("route print 0.0.0.0", shell=True).decode("gbk", errors="replace")
|
||||
for line in output.splitlines():
|
||||
if "0.0.0.0" in line and "On-link" not in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 3:
|
||||
return parts[2]
|
||||
else:
|
||||
output = subprocess.check_output("ip route show default", shell=True).decode(errors="replace")
|
||||
return output.split()[2]
|
||||
except Exception:
|
||||
return "Unknown"
|
||||
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
local_ip = socket.gethostbyname(hostname)
|
||||
mac = get_mac_address()
|
||||
gateway = get_gateway()
|
||||
public_ip = get_public_ip()
|
||||
return {
|
||||
"ok": True,
|
||||
"hostname": hostname,
|
||||
"local_ip": local_ip,
|
||||
"public_ip": public_ip,
|
||||
"mac_address": mac,
|
||||
"gateway": gateway,
|
||||
"platform": sys.platform,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": f"Failed to read local network info: {e}"}
|
||||
|
||||
return ToolSpec(
|
||||
name="get_local_net_info",
|
||||
description="Summarize local hostname, IPs, MAC, default gateway, and OS platform (best-effort).",
|
||||
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["dns_lookup_tool", "ssl_check_tool", "port_check_tool", "port_scan_tool", "local_net_info_tool"]
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
"""网络运维专家工具清单。"""
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
|
||||
|
||||
def query_route_tool() -> ToolSpec:
|
||||
from .query_route import query_route_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def get_path_tool() -> ToolSpec:
|
||||
from .get_path import get_path_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def config_diff_tool() -> ToolSpec:
|
||||
from .config_diff import config_diff_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def device_status_tool() -> ToolSpec:
|
||||
from .device_status import device_status_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def log_analysis_tool() -> ToolSpec:
|
||||
from .log_analysis import log_analysis_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def dns_lookup_tool() -> ToolSpec:
|
||||
from .network_probe_tools import dns_lookup_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def ssl_check_tool() -> ToolSpec:
|
||||
from .network_probe_tools import ssl_check_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def port_check_tool() -> ToolSpec:
|
||||
from .network_probe_tools import port_check_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def port_scan_tool() -> ToolSpec:
|
||||
from .network_probe_tools import port_scan_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
def local_net_info_tool() -> ToolSpec:
|
||||
from .network_probe_tools import local_net_info_tool as factory
|
||||
|
||||
return factory()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"query_route_tool",
|
||||
"get_path_tool",
|
||||
"config_diff_tool",
|
||||
"device_status_tool",
|
||||
"log_analysis_tool",
|
||||
"dns_lookup_tool",
|
||||
"ssl_check_tool",
|
||||
"port_check_tool",
|
||||
"port_scan_tool",
|
||||
"local_net_info_tool",
|
||||
]
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any
|
||||
|
||||
from oclaw.runtime.tools.base import ToolSpec
|
||||
|
||||
|
||||
def _pick_route(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> dict[str, Any]:
|
||||
if isinstance(ip, ipaddress.IPv4Address):
|
||||
if ip in ipaddress.ip_network("10.0.0.0/8"):
|
||||
return {"prefix": "10.0.0.0/8", "next_hop": "192.168.1.1", "out_if": "GigabitEthernet0/0"}
|
||||
if ip in ipaddress.ip_network("172.16.0.0/12"):
|
||||
return {"prefix": "172.16.0.0/12", "next_hop": "192.168.2.1", "out_if": "GigabitEthernet0/1"}
|
||||
if ip in ipaddress.ip_network("192.168.0.0/16"):
|
||||
return {"prefix": "192.168.0.0/16", "next_hop": "direct", "out_if": "Vlan10"}
|
||||
return {"prefix": "0.0.0.0/0", "next_hop": "203.0.113.1", "out_if": "GigabitEthernet1/0"}
|
||||
|
||||
if ip in ipaddress.ip_network("fc00::/7"):
|
||||
return {"prefix": "fc00::/7", "next_hop": "fe80::1", "out_if": "Vlan20"}
|
||||
return {"prefix": "::/0", "next_hop": "2001:db8::1", "out_if": "GigabitEthernet1/0"}
|
||||
|
||||
|
||||
def query_route_tool() -> ToolSpec:
|
||||
def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
destination = str(args.get("destination"))
|
||||
vrf = args.get("vrf")
|
||||
try:
|
||||
ip = ipaddress.ip_address(destination)
|
||||
except ValueError:
|
||||
return {"ok": False, "error": f"Invalid IP address: {destination}"}
|
||||
route = _pick_route(ip)
|
||||
return {"ok": True, "destination": destination, "vrf": vrf, "route": route}
|
||||
|
||||
return ToolSpec(
|
||||
name="query_route",
|
||||
description="Look up route egress and next hop for a destination IP (demo data; replace with a real device or controller API).",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"destination": {"type": "string", "description": "Destination IP address (IPv4 or IPv6)."},
|
||||
"vrf": {"type": "string", "description": "Optional VRF name."},
|
||||
},
|
||||
"required": ["destination"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["query_route_tool"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue