runspec-linux 0.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.
- runspec_linux/__init__.py +3 -0
- runspec_linux/containers.py +117 -0
- runspec_linux/files.py +72 -0
- runspec_linux/logs.py +81 -0
- runspec_linux/nc_command.py +65 -0
- runspec_linux/network.py +159 -0
- runspec_linux/runspec.toml +328 -0
- runspec_linux/security.py +70 -0
- runspec_linux/services.py +119 -0
- runspec_linux/system_info.py +140 -0
- runspec_linux-0.1.0.dist-info/METADATA +10 -0
- runspec_linux-0.1.0.dist-info/RECORD +14 -0
- runspec_linux-0.1.0.dist-info/WHEEL +4 -0
- runspec_linux-0.1.0.dist-info/entry_points.txt +22 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import runspec as rs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _docker_available() -> bool:
|
|
10
|
+
return shutil.which("docker") is not None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main_list_containers() -> None:
|
|
14
|
+
spec = rs.parse("list-containers")
|
|
15
|
+
include_all = bool(spec.all)
|
|
16
|
+
|
|
17
|
+
if not _docker_available():
|
|
18
|
+
print(json.dumps({"error": "docker not installed or not in PATH"}))
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
cmd = [
|
|
23
|
+
"docker",
|
|
24
|
+
"ps",
|
|
25
|
+
"--format",
|
|
26
|
+
"{{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\t{{.Names}}",
|
|
27
|
+
]
|
|
28
|
+
if include_all:
|
|
29
|
+
cmd.append("--all")
|
|
30
|
+
|
|
31
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
32
|
+
if result.returncode != 0:
|
|
33
|
+
print(json.dumps({"error": result.stderr.strip()}))
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
rows = []
|
|
37
|
+
for line in result.stdout.strip().splitlines():
|
|
38
|
+
parts = line.split("\t")
|
|
39
|
+
if len(parts) < 6:
|
|
40
|
+
continue
|
|
41
|
+
rows.append(
|
|
42
|
+
{
|
|
43
|
+
"id": parts[0],
|
|
44
|
+
"image": parts[1],
|
|
45
|
+
"command": parts[2].strip('"'),
|
|
46
|
+
"created": parts[3],
|
|
47
|
+
"status": parts[4],
|
|
48
|
+
"name": parts[5],
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
print(json.dumps(rows))
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(json.dumps({"error": str(e)}))
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main_container_logs() -> None:
|
|
58
|
+
spec = rs.parse("container-logs")
|
|
59
|
+
container = str(spec.container)
|
|
60
|
+
lines = int(spec.lines)
|
|
61
|
+
|
|
62
|
+
if not _docker_available():
|
|
63
|
+
print(json.dumps({"error": "docker not installed or not in PATH"}))
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
result = subprocess.run(
|
|
68
|
+
["docker", "logs", "--tail", str(lines), container],
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
)
|
|
72
|
+
# docker logs writes to stderr by default
|
|
73
|
+
output = result.stderr if result.stderr else result.stdout
|
|
74
|
+
output_lines = output.strip().splitlines()
|
|
75
|
+
print(
|
|
76
|
+
json.dumps(
|
|
77
|
+
{
|
|
78
|
+
"container": container,
|
|
79
|
+
"lines": output_lines,
|
|
80
|
+
"count": len(output_lines),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(json.dumps({"error": str(e)}))
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main_restart_container() -> None:
|
|
90
|
+
spec = rs.parse("restart-container")
|
|
91
|
+
container = str(spec.container)
|
|
92
|
+
|
|
93
|
+
if not _docker_available():
|
|
94
|
+
print(json.dumps({"error": "docker not installed or not in PATH"}))
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["docker", "restart", container],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
)
|
|
103
|
+
if result.returncode != 0:
|
|
104
|
+
print(
|
|
105
|
+
json.dumps(
|
|
106
|
+
{
|
|
107
|
+
"container": container,
|
|
108
|
+
"restarted": False,
|
|
109
|
+
"error": result.stderr.strip(),
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
print(json.dumps({"container": container, "restarted": True}))
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print(json.dumps({"error": str(e)}))
|
|
117
|
+
sys.exit(1)
|
runspec_linux/files.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tarfile
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import runspec as rs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main_find_large_files() -> None:
|
|
12
|
+
spec = rs.parse("find-large-files")
|
|
13
|
+
search_path = str(spec.path)
|
|
14
|
+
min_mb = float(spec.min_mb)
|
|
15
|
+
limit = int(spec.limit)
|
|
16
|
+
|
|
17
|
+
min_bytes = int(min_mb * 1_048_576)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
results: list[dict] = []
|
|
21
|
+
for dirpath, _dirs, filenames in os.walk(search_path):
|
|
22
|
+
for name in filenames:
|
|
23
|
+
fpath = os.path.join(dirpath, name)
|
|
24
|
+
try:
|
|
25
|
+
stat = os.stat(fpath, follow_symlinks=False)
|
|
26
|
+
if stat.st_size >= min_bytes:
|
|
27
|
+
results.append(
|
|
28
|
+
{
|
|
29
|
+
"path": fpath,
|
|
30
|
+
"size_mb": round(stat.st_size / 1_048_576, 2),
|
|
31
|
+
"modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
except OSError:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
results.sort(key=lambda r: r["size_mb"], reverse=True)
|
|
38
|
+
print(json.dumps(results[:limit]))
|
|
39
|
+
except Exception as e:
|
|
40
|
+
print(json.dumps({"error": str(e)}))
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main_backup_files() -> None:
|
|
45
|
+
spec = rs.parse("backup-files")
|
|
46
|
+
source = str(spec.source)
|
|
47
|
+
destination = str(spec.destination)
|
|
48
|
+
|
|
49
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
50
|
+
source_name = Path(source).name or "backup"
|
|
51
|
+
archive_name = f"{source_name}_{timestamp}.tar.gz"
|
|
52
|
+
archive_path = os.path.join(destination, archive_name)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
os.makedirs(destination, exist_ok=True)
|
|
56
|
+
with tarfile.open(archive_path, "w:gz") as tar:
|
|
57
|
+
tar.add(source, arcname=source_name)
|
|
58
|
+
|
|
59
|
+
size_bytes = os.path.getsize(archive_path)
|
|
60
|
+
print(
|
|
61
|
+
json.dumps(
|
|
62
|
+
{
|
|
63
|
+
"source": source,
|
|
64
|
+
"destination": archive_path,
|
|
65
|
+
"size_bytes": size_bytes,
|
|
66
|
+
"size_mb": round(size_bytes / 1_048_576, 2),
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(json.dumps({"error": str(e)}))
|
|
72
|
+
sys.exit(1)
|
runspec_linux/logs.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import runspec as rs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main_tail_log() -> None:
|
|
11
|
+
spec = rs.parse("tail-log")
|
|
12
|
+
file_path = str(spec.file)
|
|
13
|
+
lines = int(spec.lines)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
with open(file_path, errors="replace") as f:
|
|
17
|
+
all_lines = f.readlines()
|
|
18
|
+
tail = [line.rstrip("\n") for line in all_lines[-lines:]]
|
|
19
|
+
print(json.dumps({"file": file_path, "lines": tail, "count": len(tail)}))
|
|
20
|
+
except OSError as e:
|
|
21
|
+
print(json.dumps({"error": str(e), "file": file_path}))
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main_search_log() -> None:
|
|
26
|
+
spec = rs.parse("search-log")
|
|
27
|
+
file_path = str(spec.file)
|
|
28
|
+
pattern = str(spec.pattern)
|
|
29
|
+
limit = int(spec.limit)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
33
|
+
matches: list[str] = []
|
|
34
|
+
with open(file_path, errors="replace") as f:
|
|
35
|
+
for line in f:
|
|
36
|
+
if regex.search(line):
|
|
37
|
+
matches.append(line.rstrip("\n"))
|
|
38
|
+
trimmed = matches[-limit:] if len(matches) > limit else matches
|
|
39
|
+
print(
|
|
40
|
+
json.dumps(
|
|
41
|
+
{
|
|
42
|
+
"file": file_path,
|
|
43
|
+
"pattern": pattern,
|
|
44
|
+
"matches": trimmed,
|
|
45
|
+
"count": len(trimmed),
|
|
46
|
+
"total_matches": len(matches),
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
except OSError as e:
|
|
51
|
+
print(json.dumps({"error": str(e), "file": file_path}))
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
except re.error as e:
|
|
54
|
+
print(json.dumps({"error": f"Invalid regex: {e}", "pattern": pattern}))
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main_journalctl() -> None:
|
|
59
|
+
spec = rs.parse("journalctl")
|
|
60
|
+
unit = str(spec.unit)
|
|
61
|
+
lines = int(spec.lines)
|
|
62
|
+
since = str(spec.since) if spec.since is not None else None
|
|
63
|
+
|
|
64
|
+
if not shutil.which("journalctl"):
|
|
65
|
+
print(json.dumps({"error": "journalctl not available — not a systemd system"}))
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
cmd = ["journalctl", f"-u{unit}", f"-n{lines}", "--no-pager", "--output=short"]
|
|
70
|
+
if since:
|
|
71
|
+
cmd += [f"--since={since}"]
|
|
72
|
+
|
|
73
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
74
|
+
output_lines = result.stdout.strip().splitlines()
|
|
75
|
+
print(json.dumps({"unit": unit, "lines": output_lines, "count": len(output_lines)}))
|
|
76
|
+
except subprocess.CalledProcessError as e:
|
|
77
|
+
print(json.dumps({"error": e.stderr.strip(), "unit": unit}))
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(json.dumps({"error": str(e)}))
|
|
81
|
+
sys.exit(1)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import socket
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import runspec as rs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def nc_send(
|
|
10
|
+
host: str,
|
|
11
|
+
port: int,
|
|
12
|
+
command: str,
|
|
13
|
+
wait: float = 0.3,
|
|
14
|
+
read_timeout: float = 0.1,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Send a command over a plain TCP socket and return the response.
|
|
17
|
+
|
|
18
|
+
Appends a newline to command if not present, waits `wait` seconds for the
|
|
19
|
+
server to respond, then reads in chunks until the socket goes quiet.
|
|
20
|
+
"""
|
|
21
|
+
with socket.create_connection((host, port), timeout=10) as sock:
|
|
22
|
+
payload = command if command.endswith("\n") else command + "\n"
|
|
23
|
+
sock.sendall(payload.encode())
|
|
24
|
+
|
|
25
|
+
time.sleep(wait)
|
|
26
|
+
|
|
27
|
+
sock.settimeout(read_timeout)
|
|
28
|
+
chunks: list[bytes] = []
|
|
29
|
+
while True:
|
|
30
|
+
try:
|
|
31
|
+
chunk = sock.recv(4096)
|
|
32
|
+
if not chunk:
|
|
33
|
+
break
|
|
34
|
+
chunks.append(chunk)
|
|
35
|
+
except TimeoutError:
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
return b"".join(chunks).decode(errors="replace")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
spec = rs.parse("nc-command")
|
|
43
|
+
host = str(spec.host)
|
|
44
|
+
port = int(spec.port)
|
|
45
|
+
command = str(spec.command)
|
|
46
|
+
wait = float(spec.wait)
|
|
47
|
+
read_timeout = float(spec.read_timeout)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
response = nc_send(host, port, command, wait=wait, read_timeout=read_timeout)
|
|
51
|
+
lines = response.splitlines()
|
|
52
|
+
print(
|
|
53
|
+
json.dumps(
|
|
54
|
+
{
|
|
55
|
+
"host": host,
|
|
56
|
+
"port": port,
|
|
57
|
+
"command": command,
|
|
58
|
+
"stdout": response,
|
|
59
|
+
"lines": lines,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
except OSError as e:
|
|
64
|
+
print(json.dumps({"error": str(e), "host": host, "port": port}))
|
|
65
|
+
sys.exit(1)
|
runspec_linux/network.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import runspec as rs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main_ping_host() -> None:
|
|
13
|
+
spec = rs.parse("ping-host")
|
|
14
|
+
host = str(spec.host)
|
|
15
|
+
count = int(spec.count)
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
["ping", "-c", str(count), host],
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
)
|
|
23
|
+
output = result.stdout + result.stderr
|
|
24
|
+
|
|
25
|
+
sent = received = 0
|
|
26
|
+
m = re.search(r"(\d+) packets transmitted,\s*(\d+) received", output)
|
|
27
|
+
if m:
|
|
28
|
+
sent = int(m.group(1))
|
|
29
|
+
received = int(m.group(2))
|
|
30
|
+
|
|
31
|
+
loss_pct = 0.0
|
|
32
|
+
m2 = re.search(r"([\d.]+)% packet loss", output)
|
|
33
|
+
if m2:
|
|
34
|
+
loss_pct = float(m2.group(1))
|
|
35
|
+
|
|
36
|
+
print(
|
|
37
|
+
json.dumps(
|
|
38
|
+
{
|
|
39
|
+
"host": host,
|
|
40
|
+
"reachable": received > 0,
|
|
41
|
+
"packets_sent": sent,
|
|
42
|
+
"packets_received": received,
|
|
43
|
+
"loss_pct": loss_pct,
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(json.dumps({"error": str(e), "host": host}))
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main_check_port() -> None:
|
|
53
|
+
spec = rs.parse("check-port")
|
|
54
|
+
host = str(spec.host)
|
|
55
|
+
port = int(spec.port)
|
|
56
|
+
timeout = float(spec.timeout)
|
|
57
|
+
|
|
58
|
+
start = time.monotonic()
|
|
59
|
+
try:
|
|
60
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
61
|
+
elapsed_ms = round((time.monotonic() - start) * 1000, 1)
|
|
62
|
+
print(
|
|
63
|
+
json.dumps(
|
|
64
|
+
{
|
|
65
|
+
"host": host,
|
|
66
|
+
"port": port,
|
|
67
|
+
"open": True,
|
|
68
|
+
"response_ms": elapsed_ms,
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
except (OSError, TimeoutError):
|
|
73
|
+
elapsed_ms = round((time.monotonic() - start) * 1000, 1)
|
|
74
|
+
print(json.dumps({"host": host, "port": port, "open": False, "response_ms": elapsed_ms}))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main_show_connections() -> None:
|
|
78
|
+
spec = rs.parse("show-connections")
|
|
79
|
+
state_filter = str(spec.state)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
if shutil.which("ss"):
|
|
83
|
+
cmd = ["ss", "-tupn"]
|
|
84
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
85
|
+
rows = _parse_ss(result.stdout, state_filter)
|
|
86
|
+
elif shutil.which("netstat"):
|
|
87
|
+
cmd = ["netstat", "-tupn"]
|
|
88
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
89
|
+
rows = _parse_netstat(result.stdout, state_filter)
|
|
90
|
+
else:
|
|
91
|
+
print(json.dumps({"error": "Neither ss nor netstat found in PATH"}))
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
print(json.dumps(rows))
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(json.dumps({"error": str(e)}))
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_ss(output: str, state_filter: str) -> list[dict]:
|
|
101
|
+
rows = []
|
|
102
|
+
for line in output.strip().splitlines()[1:]:
|
|
103
|
+
parts = line.split()
|
|
104
|
+
if len(parts) < 5:
|
|
105
|
+
continue
|
|
106
|
+
proto, local, peer = parts[0], parts[3], parts[4]
|
|
107
|
+
state = parts[5] if len(parts) > 5 and not parts[5].startswith("users:") else ""
|
|
108
|
+
process = ""
|
|
109
|
+
for p in parts:
|
|
110
|
+
if p.startswith("users:"):
|
|
111
|
+
m = re.search(r'"([^"]+)"', p)
|
|
112
|
+
if m:
|
|
113
|
+
process = m.group(1)
|
|
114
|
+
|
|
115
|
+
row_state = state.lower() if state else ("listen" if peer == "*:*" else "established")
|
|
116
|
+
if state_filter == "established" and "estab" not in row_state:
|
|
117
|
+
continue
|
|
118
|
+
if state_filter == "listening" and "listen" not in row_state:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
rows.append(
|
|
122
|
+
{
|
|
123
|
+
"proto": proto,
|
|
124
|
+
"local": local,
|
|
125
|
+
"peer": peer,
|
|
126
|
+
"state": state,
|
|
127
|
+
"process": process,
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
return rows
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_netstat(output: str, state_filter: str) -> list[dict]:
|
|
134
|
+
rows = []
|
|
135
|
+
for line in output.strip().splitlines():
|
|
136
|
+
parts = line.split()
|
|
137
|
+
if not parts or parts[0] not in ("tcp", "tcp6", "udp", "udp6"):
|
|
138
|
+
continue
|
|
139
|
+
if len(parts) < 6:
|
|
140
|
+
continue
|
|
141
|
+
proto, local, foreign = parts[0], parts[3], parts[4]
|
|
142
|
+
state = parts[5] if len(parts) > 5 else ""
|
|
143
|
+
process = parts[-1] if len(parts) > 6 else ""
|
|
144
|
+
|
|
145
|
+
if state_filter == "established" and state.upper() != "ESTABLISHED":
|
|
146
|
+
continue
|
|
147
|
+
if state_filter == "listening" and state.upper() != "LISTEN":
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
rows.append(
|
|
151
|
+
{
|
|
152
|
+
"proto": proto,
|
|
153
|
+
"local": local,
|
|
154
|
+
"peer": foreign,
|
|
155
|
+
"state": state,
|
|
156
|
+
"process": process,
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
return rows
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
[config]
|
|
2
|
+
autonomy-default = "autonomous"
|
|
3
|
+
|
|
4
|
+
# ── System monitoring ─────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
[system-info]
|
|
7
|
+
serve = ["local"]
|
|
8
|
+
description = "Report hostname, OS, kernel version, uptime, load averages, and CPU count"
|
|
9
|
+
autonomy = "autonomous"
|
|
10
|
+
output = "json"
|
|
11
|
+
|
|
12
|
+
[disk-usage]
|
|
13
|
+
serve = ["local"]
|
|
14
|
+
description = "Report disk usage for all mounted filesystems"
|
|
15
|
+
autonomy = "autonomous"
|
|
16
|
+
output = "json"
|
|
17
|
+
|
|
18
|
+
[check-memory]
|
|
19
|
+
serve = ["local"]
|
|
20
|
+
description = "Report RAM and swap usage in megabytes"
|
|
21
|
+
autonomy = "autonomous"
|
|
22
|
+
output = "json"
|
|
23
|
+
|
|
24
|
+
[list-processes]
|
|
25
|
+
serve = ["local"]
|
|
26
|
+
description = "List running processes, optionally filtered by name"
|
|
27
|
+
autonomy = "autonomous"
|
|
28
|
+
output = "json"
|
|
29
|
+
|
|
30
|
+
[list-processes.args.filter]
|
|
31
|
+
type = "str"
|
|
32
|
+
short = "-f"
|
|
33
|
+
description = "Filter processes whose command contains this string"
|
|
34
|
+
required = false
|
|
35
|
+
|
|
36
|
+
[list-processes.args.limit]
|
|
37
|
+
type = "int"
|
|
38
|
+
short = "-n"
|
|
39
|
+
description = "Maximum number of processes to return"
|
|
40
|
+
default = 20
|
|
41
|
+
|
|
42
|
+
# ── Services ──────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
[check-service]
|
|
45
|
+
serve = ["local"]
|
|
46
|
+
description = "Check whether a systemd service is active and report its status"
|
|
47
|
+
autonomy = "autonomous"
|
|
48
|
+
output = "json"
|
|
49
|
+
|
|
50
|
+
[check-service.args.service]
|
|
51
|
+
type = "str"
|
|
52
|
+
short = "-s"
|
|
53
|
+
description = "Service name (e.g. nginx or nginx.service)"
|
|
54
|
+
|
|
55
|
+
[list-services]
|
|
56
|
+
serve = ["local"]
|
|
57
|
+
description = "List all systemd service units and their active/sub state"
|
|
58
|
+
autonomy = "autonomous"
|
|
59
|
+
output = "json"
|
|
60
|
+
|
|
61
|
+
[list-services.args.state]
|
|
62
|
+
type = "choice"
|
|
63
|
+
options = ["all", "active", "failed", "inactive"]
|
|
64
|
+
default = "all"
|
|
65
|
+
description = "Filter services by active state"
|
|
66
|
+
|
|
67
|
+
[restart-service]
|
|
68
|
+
serve = ["local"]
|
|
69
|
+
description = "Restart a systemd service"
|
|
70
|
+
autonomy = "confirm"
|
|
71
|
+
output = "json"
|
|
72
|
+
|
|
73
|
+
[restart-service.args.service]
|
|
74
|
+
type = "str"
|
|
75
|
+
short = "-s"
|
|
76
|
+
description = "Service name to restart"
|
|
77
|
+
|
|
78
|
+
# ── Logs ──────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
[tail-log]
|
|
81
|
+
serve = ["local"]
|
|
82
|
+
description = "Return the last N lines of a log file"
|
|
83
|
+
autonomy = "autonomous"
|
|
84
|
+
output = "json"
|
|
85
|
+
|
|
86
|
+
[tail-log.args.file]
|
|
87
|
+
type = "path"
|
|
88
|
+
short = "-f"
|
|
89
|
+
description = "Path to the log file"
|
|
90
|
+
|
|
91
|
+
[tail-log.args.lines]
|
|
92
|
+
type = "int"
|
|
93
|
+
short = "-n"
|
|
94
|
+
description = "Number of lines to return"
|
|
95
|
+
default = 50
|
|
96
|
+
|
|
97
|
+
[search-log]
|
|
98
|
+
serve = ["local"]
|
|
99
|
+
description = "Search a log file for lines matching a pattern"
|
|
100
|
+
autonomy = "autonomous"
|
|
101
|
+
output = "json"
|
|
102
|
+
|
|
103
|
+
[search-log.args.file]
|
|
104
|
+
type = "path"
|
|
105
|
+
short = "-f"
|
|
106
|
+
description = "Path to the log file"
|
|
107
|
+
|
|
108
|
+
[search-log.args.pattern]
|
|
109
|
+
type = "str"
|
|
110
|
+
short = "-p"
|
|
111
|
+
description = "Search pattern (case-insensitive substring or regex)"
|
|
112
|
+
|
|
113
|
+
[search-log.args.limit]
|
|
114
|
+
type = "int"
|
|
115
|
+
short = "-n"
|
|
116
|
+
description = "Maximum number of matching lines to return"
|
|
117
|
+
default = 100
|
|
118
|
+
|
|
119
|
+
[journalctl]
|
|
120
|
+
serve = ["local"]
|
|
121
|
+
description = "Fetch recent journal entries for a systemd unit (systemd-only)"
|
|
122
|
+
autonomy = "autonomous"
|
|
123
|
+
output = "json"
|
|
124
|
+
|
|
125
|
+
[journalctl.args.unit]
|
|
126
|
+
type = "str"
|
|
127
|
+
short = "-u"
|
|
128
|
+
description = "Systemd unit name (e.g. nginx or nginx.service)"
|
|
129
|
+
|
|
130
|
+
[journalctl.args.lines]
|
|
131
|
+
type = "int"
|
|
132
|
+
short = "-n"
|
|
133
|
+
description = "Number of lines to return"
|
|
134
|
+
default = 50
|
|
135
|
+
|
|
136
|
+
[journalctl.args.since]
|
|
137
|
+
type = "str"
|
|
138
|
+
description = "Return entries since this time string (e.g. '1 hour ago', '2026-05-27 08:00')"
|
|
139
|
+
required = false
|
|
140
|
+
|
|
141
|
+
# ── Network ───────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
[ping-host]
|
|
144
|
+
serve = ["local"]
|
|
145
|
+
description = "Ping a host and report reachability and packet loss"
|
|
146
|
+
autonomy = "autonomous"
|
|
147
|
+
output = "json"
|
|
148
|
+
|
|
149
|
+
[ping-host.args.host]
|
|
150
|
+
type = "str"
|
|
151
|
+
short = "-H"
|
|
152
|
+
description = "Hostname or IP address to ping"
|
|
153
|
+
|
|
154
|
+
[ping-host.args.count]
|
|
155
|
+
type = "int"
|
|
156
|
+
short = "-c"
|
|
157
|
+
description = "Number of ICMP packets to send"
|
|
158
|
+
default = 4
|
|
159
|
+
|
|
160
|
+
[check-port]
|
|
161
|
+
serve = ["local"]
|
|
162
|
+
description = "Check whether a TCP port is open on a host"
|
|
163
|
+
autonomy = "autonomous"
|
|
164
|
+
output = "json"
|
|
165
|
+
|
|
166
|
+
[check-port.args.host]
|
|
167
|
+
type = "str"
|
|
168
|
+
short = "-H"
|
|
169
|
+
description = "Hostname or IP address"
|
|
170
|
+
|
|
171
|
+
[check-port.args.port]
|
|
172
|
+
type = "int"
|
|
173
|
+
short = "-p"
|
|
174
|
+
description = "TCP port number"
|
|
175
|
+
|
|
176
|
+
[check-port.args.timeout]
|
|
177
|
+
type = "float"
|
|
178
|
+
description = "Connection timeout in seconds"
|
|
179
|
+
default = 3.0
|
|
180
|
+
|
|
181
|
+
[show-connections]
|
|
182
|
+
serve = ["local"]
|
|
183
|
+
description = "Show active TCP/UDP connections (equivalent to ss or netstat)"
|
|
184
|
+
autonomy = "autonomous"
|
|
185
|
+
output = "json"
|
|
186
|
+
|
|
187
|
+
[show-connections.args.state]
|
|
188
|
+
type = "choice"
|
|
189
|
+
options = ["all", "established", "listening"]
|
|
190
|
+
default = "all"
|
|
191
|
+
description = "Filter connections by state"
|
|
192
|
+
|
|
193
|
+
# ── Files ─────────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
[find-large-files]
|
|
196
|
+
serve = ["local"]
|
|
197
|
+
description = "Find files larger than a threshold in a directory tree"
|
|
198
|
+
autonomy = "autonomous"
|
|
199
|
+
output = "json"
|
|
200
|
+
|
|
201
|
+
[find-large-files.args.path]
|
|
202
|
+
type = "path"
|
|
203
|
+
short = "-p"
|
|
204
|
+
description = "Directory to search"
|
|
205
|
+
|
|
206
|
+
[find-large-files.args.min-mb]
|
|
207
|
+
type = "float"
|
|
208
|
+
description = "Minimum file size in megabytes"
|
|
209
|
+
default = 100.0
|
|
210
|
+
|
|
211
|
+
[find-large-files.args.limit]
|
|
212
|
+
type = "int"
|
|
213
|
+
short = "-n"
|
|
214
|
+
description = "Maximum number of results to return (sorted by size, largest first)"
|
|
215
|
+
default = 20
|
|
216
|
+
|
|
217
|
+
[backup-files]
|
|
218
|
+
serve = ["local"]
|
|
219
|
+
description = "Create a timestamped tar.gz archive of a directory"
|
|
220
|
+
autonomy = "confirm"
|
|
221
|
+
output = "json"
|
|
222
|
+
|
|
223
|
+
[backup-files.args.source]
|
|
224
|
+
type = "path"
|
|
225
|
+
short = "-s"
|
|
226
|
+
description = "Directory to archive"
|
|
227
|
+
|
|
228
|
+
[backup-files.args.destination]
|
|
229
|
+
type = "path"
|
|
230
|
+
short = "-d"
|
|
231
|
+
description = "Directory where the archive will be written"
|
|
232
|
+
default = "/tmp"
|
|
233
|
+
|
|
234
|
+
# ── Security ──────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
[last-logins]
|
|
237
|
+
serve = ["local"]
|
|
238
|
+
description = "Show recent login history from the wtmp log"
|
|
239
|
+
autonomy = "autonomous"
|
|
240
|
+
output = "json"
|
|
241
|
+
|
|
242
|
+
[last-logins.args.limit]
|
|
243
|
+
type = "int"
|
|
244
|
+
short = "-n"
|
|
245
|
+
description = "Number of login records to return"
|
|
246
|
+
default = 20
|
|
247
|
+
|
|
248
|
+
[who]
|
|
249
|
+
serve = ["local"]
|
|
250
|
+
description = "List users currently logged in to the system"
|
|
251
|
+
autonomy = "autonomous"
|
|
252
|
+
output = "json"
|
|
253
|
+
|
|
254
|
+
# ── Containers ────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
[list-containers]
|
|
257
|
+
serve = ["local"]
|
|
258
|
+
description = "List Docker containers (requires Docker)"
|
|
259
|
+
autonomy = "autonomous"
|
|
260
|
+
output = "json"
|
|
261
|
+
|
|
262
|
+
[list-containers.args.all]
|
|
263
|
+
type = "flag"
|
|
264
|
+
short = "-a"
|
|
265
|
+
description = "Include stopped containers"
|
|
266
|
+
default = false
|
|
267
|
+
|
|
268
|
+
[container-logs]
|
|
269
|
+
serve = ["local"]
|
|
270
|
+
description = "Fetch recent log output from a Docker container"
|
|
271
|
+
autonomy = "autonomous"
|
|
272
|
+
output = "json"
|
|
273
|
+
|
|
274
|
+
[container-logs.args.container]
|
|
275
|
+
type = "str"
|
|
276
|
+
short = "-c"
|
|
277
|
+
description = "Container name or ID"
|
|
278
|
+
|
|
279
|
+
[container-logs.args.lines]
|
|
280
|
+
type = "int"
|
|
281
|
+
short = "-n"
|
|
282
|
+
description = "Number of lines to return"
|
|
283
|
+
default = 50
|
|
284
|
+
|
|
285
|
+
[restart-container]
|
|
286
|
+
serve = ["local"]
|
|
287
|
+
description = "Restart a Docker container"
|
|
288
|
+
autonomy = "confirm"
|
|
289
|
+
output = "json"
|
|
290
|
+
|
|
291
|
+
[restart-container.args.container]
|
|
292
|
+
type = "str"
|
|
293
|
+
short = "-c"
|
|
294
|
+
description = "Container name or ID"
|
|
295
|
+
|
|
296
|
+
# ── TCP interfaces (nc-command) ────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
[nc-command]
|
|
299
|
+
serve = ["local"]
|
|
300
|
+
description = "Send a command over a plain TCP socket and return the response (nc-style)"
|
|
301
|
+
autonomy = "autonomous"
|
|
302
|
+
output = "json"
|
|
303
|
+
|
|
304
|
+
[nc-command.args.host]
|
|
305
|
+
type = "str"
|
|
306
|
+
short = "-H"
|
|
307
|
+
description = "Hostname or IP address"
|
|
308
|
+
|
|
309
|
+
[nc-command.args.port]
|
|
310
|
+
type = "int"
|
|
311
|
+
short = "-p"
|
|
312
|
+
description = "TCP port number"
|
|
313
|
+
|
|
314
|
+
[nc-command.args.command]
|
|
315
|
+
type = "str"
|
|
316
|
+
short = "-c"
|
|
317
|
+
description = "Command string to send (a newline is appended automatically)"
|
|
318
|
+
|
|
319
|
+
[nc-command.args.wait]
|
|
320
|
+
type = "float"
|
|
321
|
+
short = "-w"
|
|
322
|
+
description = "Seconds to keep the connection open after sending (allows server response to arrive)"
|
|
323
|
+
default = 0.3
|
|
324
|
+
|
|
325
|
+
[nc-command.args.read-timeout]
|
|
326
|
+
type = "float"
|
|
327
|
+
description = "Per-chunk socket read timeout in seconds"
|
|
328
|
+
default = 0.1
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import runspec as rs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main_last_logins() -> None:
|
|
10
|
+
spec = rs.parse("last-logins")
|
|
11
|
+
limit = int(spec.limit)
|
|
12
|
+
|
|
13
|
+
if not shutil.which("last"):
|
|
14
|
+
print(json.dumps({"error": "last command not found"}))
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
["last", "-n", str(limit), "--time-format", "iso"],
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
)
|
|
23
|
+
rows = []
|
|
24
|
+
for line in result.stdout.strip().splitlines():
|
|
25
|
+
if not line or line.startswith("wtmp") or line.startswith("btmp"):
|
|
26
|
+
continue
|
|
27
|
+
parts = line.split()
|
|
28
|
+
if len(parts) < 3:
|
|
29
|
+
continue
|
|
30
|
+
rows.append(
|
|
31
|
+
{
|
|
32
|
+
"user": parts[0],
|
|
33
|
+
"terminal": parts[1],
|
|
34
|
+
"from": parts[2] if len(parts) > 2 else "",
|
|
35
|
+
"login_time": parts[3] if len(parts) > 3 else "",
|
|
36
|
+
"logout_time": parts[5] if len(parts) > 5 else "",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
print(json.dumps(rows[:limit]))
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(json.dumps({"error": str(e)}))
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main_who() -> None:
|
|
46
|
+
rs.parse("who")
|
|
47
|
+
|
|
48
|
+
if not shutil.which("who"):
|
|
49
|
+
print(json.dumps({"error": "who command not found"}))
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
result = subprocess.run(["who"], capture_output=True, text=True, check=True)
|
|
54
|
+
rows = []
|
|
55
|
+
for line in result.stdout.strip().splitlines():
|
|
56
|
+
parts = line.split()
|
|
57
|
+
if len(parts) < 3:
|
|
58
|
+
continue
|
|
59
|
+
rows.append(
|
|
60
|
+
{
|
|
61
|
+
"user": parts[0],
|
|
62
|
+
"terminal": parts[1],
|
|
63
|
+
"login_time": " ".join(parts[2:4]) if len(parts) > 3 else parts[2],
|
|
64
|
+
"from": parts[4].strip("()") if len(parts) > 4 else "",
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
print(json.dumps(rows))
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(json.dumps({"error": str(e)}))
|
|
70
|
+
sys.exit(1)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import runspec as rs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _systemctl_available() -> bool:
|
|
10
|
+
return shutil.which("systemctl") is not None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main_check_service() -> None:
|
|
14
|
+
spec = rs.parse("check-service")
|
|
15
|
+
service = str(spec.service)
|
|
16
|
+
|
|
17
|
+
if not _systemctl_available():
|
|
18
|
+
print(json.dumps({"error": "systemctl not available — not a systemd system"}))
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
[
|
|
24
|
+
"systemctl",
|
|
25
|
+
"show",
|
|
26
|
+
service,
|
|
27
|
+
"--property=ActiveState,SubState,LoadState,Description,ActiveEnterTimestamp",
|
|
28
|
+
],
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
)
|
|
32
|
+
props: dict[str, str] = {}
|
|
33
|
+
for line in result.stdout.strip().splitlines():
|
|
34
|
+
if "=" in line:
|
|
35
|
+
k, v = line.split("=", 1)
|
|
36
|
+
props[k] = v
|
|
37
|
+
|
|
38
|
+
active = props.get("ActiveState", "unknown")
|
|
39
|
+
print(
|
|
40
|
+
json.dumps(
|
|
41
|
+
{
|
|
42
|
+
"service": service,
|
|
43
|
+
"active": active == "active",
|
|
44
|
+
"active_state": active,
|
|
45
|
+
"sub_state": props.get("SubState", "unknown"),
|
|
46
|
+
"load_state": props.get("LoadState", "unknown"),
|
|
47
|
+
"description": props.get("Description", ""),
|
|
48
|
+
"since": props.get("ActiveEnterTimestamp", ""),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(json.dumps({"error": str(e)}))
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main_list_services() -> None:
|
|
58
|
+
spec = rs.parse("list-services")
|
|
59
|
+
state_filter = str(spec.state)
|
|
60
|
+
|
|
61
|
+
if not _systemctl_available():
|
|
62
|
+
print(json.dumps({"error": "systemctl not available — not a systemd system"}))
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
cmd = ["systemctl", "list-units", "--type=service", "--no-pager", "--no-legend"]
|
|
67
|
+
if state_filter != "all":
|
|
68
|
+
cmd += [f"--state={state_filter}"]
|
|
69
|
+
|
|
70
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
71
|
+
rows = []
|
|
72
|
+
for line in result.stdout.strip().splitlines():
|
|
73
|
+
parts = line.split(None, 4)
|
|
74
|
+
if len(parts) < 4:
|
|
75
|
+
continue
|
|
76
|
+
rows.append(
|
|
77
|
+
{
|
|
78
|
+
"unit": parts[0],
|
|
79
|
+
"load": parts[1],
|
|
80
|
+
"active": parts[2],
|
|
81
|
+
"sub": parts[3],
|
|
82
|
+
"description": parts[4] if len(parts) > 4 else "",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
print(json.dumps(rows))
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(json.dumps({"error": str(e)}))
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main_restart_service() -> None:
|
|
92
|
+
spec = rs.parse("restart-service")
|
|
93
|
+
service = str(spec.service)
|
|
94
|
+
|
|
95
|
+
if not _systemctl_available():
|
|
96
|
+
print(json.dumps({"error": "systemctl not available — not a systemd system"}))
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
["systemctl", "restart", service],
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
)
|
|
105
|
+
if result.returncode != 0:
|
|
106
|
+
print(
|
|
107
|
+
json.dumps(
|
|
108
|
+
{
|
|
109
|
+
"service": service,
|
|
110
|
+
"restarted": False,
|
|
111
|
+
"error": result.stderr.strip(),
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
print(json.dumps({"service": service, "restarted": True}))
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(json.dumps({"error": str(e)}))
|
|
119
|
+
sys.exit(1)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import runspec as rs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main_system_info() -> None:
|
|
10
|
+
rs.parse("system-info")
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
load1, load5, load15 = os.getloadavg()
|
|
14
|
+
with open("/proc/uptime") as f:
|
|
15
|
+
uptime_seconds = float(f.read().split()[0])
|
|
16
|
+
|
|
17
|
+
print(
|
|
18
|
+
json.dumps(
|
|
19
|
+
{
|
|
20
|
+
"hostname": platform.node(),
|
|
21
|
+
"os": f"{platform.system()} {platform.release()}",
|
|
22
|
+
"kernel": platform.version(),
|
|
23
|
+
"machine": platform.machine(),
|
|
24
|
+
"uptime_seconds": round(uptime_seconds),
|
|
25
|
+
"load_1": round(load1, 2),
|
|
26
|
+
"load_5": round(load5, 2),
|
|
27
|
+
"load_15": round(load15, 2),
|
|
28
|
+
"cpu_count": os.cpu_count(),
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
print(json.dumps({"error": str(e)}))
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main_disk_usage() -> None:
|
|
38
|
+
rs.parse("disk-usage")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import subprocess
|
|
42
|
+
|
|
43
|
+
result = subprocess.run(
|
|
44
|
+
["df", "-P", "-B1"],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
check=True,
|
|
48
|
+
)
|
|
49
|
+
rows = []
|
|
50
|
+
for line in result.stdout.strip().splitlines()[1:]:
|
|
51
|
+
parts = line.split()
|
|
52
|
+
if len(parts) < 6:
|
|
53
|
+
continue
|
|
54
|
+
size_bytes = int(parts[1])
|
|
55
|
+
used_bytes = int(parts[2])
|
|
56
|
+
avail_bytes = int(parts[3])
|
|
57
|
+
rows.append(
|
|
58
|
+
{
|
|
59
|
+
"filesystem": parts[0],
|
|
60
|
+
"size_mb": round(size_bytes / 1_048_576, 1),
|
|
61
|
+
"used_mb": round(used_bytes / 1_048_576, 1),
|
|
62
|
+
"available_mb": round(avail_bytes / 1_048_576, 1),
|
|
63
|
+
"use_pct": parts[4],
|
|
64
|
+
"mounted_on": parts[5],
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
print(json.dumps(rows))
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(json.dumps({"error": str(e)}))
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main_check_memory() -> None:
|
|
74
|
+
rs.parse("check-memory")
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
mem: dict[str, int] = {}
|
|
78
|
+
with open("/proc/meminfo") as f:
|
|
79
|
+
for line in f:
|
|
80
|
+
key, val_kb = line.split(":")
|
|
81
|
+
kb = int(val_kb.strip().split()[0])
|
|
82
|
+
mem[key.strip()] = kb
|
|
83
|
+
|
|
84
|
+
print(
|
|
85
|
+
json.dumps(
|
|
86
|
+
{
|
|
87
|
+
"total_mb": round(mem.get("MemTotal", 0) / 1024, 1),
|
|
88
|
+
"used_mb": round((mem.get("MemTotal", 0) - mem.get("MemAvailable", 0)) / 1024, 1),
|
|
89
|
+
"free_mb": round(mem.get("MemFree", 0) / 1024, 1),
|
|
90
|
+
"available_mb": round(mem.get("MemAvailable", 0) / 1024, 1),
|
|
91
|
+
"cached_mb": round(mem.get("Cached", 0) / 1024, 1),
|
|
92
|
+
"swap_total_mb": round(mem.get("SwapTotal", 0) / 1024, 1),
|
|
93
|
+
"swap_used_mb": round((mem.get("SwapTotal", 0) - mem.get("SwapFree", 0)) / 1024, 1),
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(json.dumps({"error": str(e)}))
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main_list_processes() -> None:
|
|
103
|
+
spec = rs.parse("list-processes")
|
|
104
|
+
name_filter = str(spec.filter) if spec.filter is not None else None
|
|
105
|
+
limit = int(spec.limit)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
import subprocess
|
|
109
|
+
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
["ps", "aux", "--no-headers"],
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
check=True,
|
|
115
|
+
)
|
|
116
|
+
rows = []
|
|
117
|
+
for line in result.stdout.strip().splitlines():
|
|
118
|
+
parts = line.split(None, 10)
|
|
119
|
+
if len(parts) < 11:
|
|
120
|
+
continue
|
|
121
|
+
command = parts[10]
|
|
122
|
+
if name_filter and name_filter.lower() not in command.lower():
|
|
123
|
+
continue
|
|
124
|
+
rows.append(
|
|
125
|
+
{
|
|
126
|
+
"user": parts[0],
|
|
127
|
+
"pid": int(parts[1]),
|
|
128
|
+
"cpu_pct": float(parts[2]),
|
|
129
|
+
"mem_pct": float(parts[3]),
|
|
130
|
+
"vsz_kb": int(parts[4]),
|
|
131
|
+
"rss_kb": int(parts[5]),
|
|
132
|
+
"stat": parts[7],
|
|
133
|
+
"command": command,
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
rows.sort(key=lambda r: r["cpu_pct"], reverse=True)
|
|
137
|
+
print(json.dumps(rows[:limit]))
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(json.dumps({"error": str(e)}))
|
|
140
|
+
sys.exit(1)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runspec-linux
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Linux system admin runnables for runspec
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: runspec>=0.17.0
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
9
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
10
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
runspec_linux/__init__.py,sha256=4Ip3UjHGdqM7G8KUd9Z0M-DU89a1S0UWZCy0mrvT3MA,68
|
|
2
|
+
runspec_linux/containers.py,sha256=aNx6F9Wsaw7MEezRLr9UrS8_DxFsPO7LLO82qLycOdY,3279
|
|
3
|
+
runspec_linux/files.py,sha256=Dx6SmewG0LIvTVRedZnCET31TM7OKVWeviWrJrxjUgc,2257
|
|
4
|
+
runspec_linux/logs.py,sha256=QQeQT8hbLxKrVzv9myPoZW6H3CjQw--t3WL_nhy031E,2562
|
|
5
|
+
runspec_linux/nc_command.py,sha256=32X7IGrXq6lF3Xuvgy9wW5SqhUT7wC_FGeDkljEtySs,1733
|
|
6
|
+
runspec_linux/network.py,sha256=YrkQc9ISODnNUytaRYyW3ptNzFfcdxhgFxGwNiAcNG8,4750
|
|
7
|
+
runspec_linux/runspec.toml,sha256=IllfzWlJ9lFx_QhyMARAYLjkaAh-Zkzum_rn5Bt3dR0,9289
|
|
8
|
+
runspec_linux/security.py,sha256=Zy6fKLLCZXgcIQoenxcetNWsHNZxMbsJhR0x-87FLO8,2085
|
|
9
|
+
runspec_linux/services.py,sha256=tjZjaVLuHKS4EN9l0vKRjymfLpgvJ0t6AIpZivF71MI,3565
|
|
10
|
+
runspec_linux/system_info.py,sha256=CN2E_hiHeZG-BobfSCpqsFNRu_uKlp4SQgRsN084_M4,4399
|
|
11
|
+
runspec_linux-0.1.0.dist-info/METADATA,sha256=mkwJ0DZygQEPJD8vlz6Q85rPXWVC6YyNNwxDdcLyNE8,297
|
|
12
|
+
runspec_linux-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
13
|
+
runspec_linux-0.1.0.dist-info/entry_points.txt,sha256=_QpmTRGmjww1oJ7Ze8kl63x-_fZycJPQGf0V7Jxu8EQ,1176
|
|
14
|
+
runspec_linux-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
backup-files = runspec_linux.files:main_backup_files
|
|
3
|
+
check-memory = runspec_linux.system_info:main_check_memory
|
|
4
|
+
check-port = runspec_linux.network:main_check_port
|
|
5
|
+
check-service = runspec_linux.services:main_check_service
|
|
6
|
+
container-logs = runspec_linux.containers:main_container_logs
|
|
7
|
+
disk-usage = runspec_linux.system_info:main_disk_usage
|
|
8
|
+
find-large-files = runspec_linux.files:main_find_large_files
|
|
9
|
+
journalctl = runspec_linux.logs:main_journalctl
|
|
10
|
+
last-logins = runspec_linux.security:main_last_logins
|
|
11
|
+
list-containers = runspec_linux.containers:main_list_containers
|
|
12
|
+
list-processes = runspec_linux.system_info:main_list_processes
|
|
13
|
+
list-services = runspec_linux.services:main_list_services
|
|
14
|
+
nc-command = runspec_linux.nc_command:main
|
|
15
|
+
ping-host = runspec_linux.network:main_ping_host
|
|
16
|
+
restart-container = runspec_linux.containers:main_restart_container
|
|
17
|
+
restart-service = runspec_linux.services:main_restart_service
|
|
18
|
+
search-log = runspec_linux.logs:main_search_log
|
|
19
|
+
show-connections = runspec_linux.network:main_show_connections
|
|
20
|
+
system-info = runspec_linux.system_info:main_system_info
|
|
21
|
+
tail-log = runspec_linux.logs:main_tail_log
|
|
22
|
+
who = runspec_linux.security:main_who
|