90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
|
|
"""Run a command on the PVE node via the same VNC term channel the web console uses.
|
||
|
|
|
||
|
|
Usage: python3 pve_term_exec.py "<command>"
|
||
|
|
Reads ticket from /tmp/pve_t.txt (refreshed automatically if expired).
|
||
|
|
Prints the command output captured from the terminal stream.
|
||
|
|
"""
|
||
|
|
import base64
|
||
|
|
import json
|
||
|
|
import ssl
|
||
|
|
import struct
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
import urllib.parse
|
||
|
|
import urllib.request
|
||
|
|
|
||
|
|
HOST = "https://192.168.1.51:8006"
|
||
|
|
NODE = "starhe"
|
||
|
|
CTX = ssl.create_default_context()
|
||
|
|
CTX.check_hostname = False
|
||
|
|
CTX.verify_mode = ssl.CERT_NONE
|
||
|
|
|
||
|
|
|
||
|
|
def http(path, data=None, ticket=None, csrf=None):
|
||
|
|
body = urllib.parse.urlencode(data).encode() if data is not None else None
|
||
|
|
req = urllib.request.Request(HOST + path, body)
|
||
|
|
if ticket:
|
||
|
|
req.add_header("Cookie", "PVEAuthCookie=" + ticket)
|
||
|
|
if csrf:
|
||
|
|
req.add_header("CSRFPreventionToken", csrf)
|
||
|
|
with urllib.request.urlopen(req, context=CTX, timeout=30) as r:
|
||
|
|
raw = r.read()
|
||
|
|
return json.loads(raw)["data"] if raw else None
|
||
|
|
|
||
|
|
|
||
|
|
def login():
|
||
|
|
d = http("/api2/json/access/ticket",
|
||
|
|
{"username": "root@pam", "password": "yx66668888"})
|
||
|
|
return d["ticket"], d["CSRFPreventionToken"]
|
||
|
|
|
||
|
|
|
||
|
|
def run_cmd(command, timeout=60):
|
||
|
|
ticket, csrf = login()
|
||
|
|
# 1. open a term proxy (VNC channel to node shell)
|
||
|
|
r = http("/api2/json/nodes/%s/termproxy" % NODE, {}, ticket, csrf)
|
||
|
|
port, ticket_vnc = r["port"], r["ticket"]
|
||
|
|
user, realm_sep, _ = "root", "@", "pam"
|
||
|
|
|
||
|
|
import socket
|
||
|
|
sock = socket.create_connection(("192.168.1.51", port), timeout=10)
|
||
|
|
# PVE handshake: username@realm\n ticket\n binary handshake
|
||
|
|
sock.sendall(b"root@pam\n" + ticket_vnc.encode() + b"\n")
|
||
|
|
gh = sock.recv(16)
|
||
|
|
maj, mino = struct.unpack("BB", gh[0:2])
|
||
|
|
said = sock.recv(16 * 4)
|
||
|
|
# client sends its own handshake
|
||
|
|
ch = struct.pack("BB", maj, mino) + b"PVEVNC Client 00" + b"\x00" * 0
|
||
|
|
sock.sendall(ch)
|
||
|
|
|
||
|
|
# 2. run the command in the shell by typing it and reading output
|
||
|
|
marker = "PVEDONE%s" % int(time.time())
|
||
|
|
payload = command + " ; echo " + marker + "\n"
|
||
|
|
time.sleep(0.5)
|
||
|
|
sock.sendall(payload.encode())
|
||
|
|
|
||
|
|
buf = b""
|
||
|
|
deadline = time.time() + timeout
|
||
|
|
while time.time() < deadline:
|
||
|
|
try:
|
||
|
|
sock.settimeout(2)
|
||
|
|
chunk = sock.recv(65536)
|
||
|
|
if not chunk:
|
||
|
|
break
|
||
|
|
buf += chunk
|
||
|
|
if marker.encode() in buf.replace(b"\x1b", b"").replace(b"[?2004l", b""):
|
||
|
|
break
|
||
|
|
except socket.timeout:
|
||
|
|
if marker.encode() in buf:
|
||
|
|
break
|
||
|
|
continue
|
||
|
|
sock.close()
|
||
|
|
# strip terminal control chars roughly and the echoed command
|
||
|
|
text = buf.decode("utf-8", "replace")
|
||
|
|
text = text.replace(marker, "")
|
||
|
|
lines = [l for l in text.split("\r\n")]
|
||
|
|
return "\n".join(lines)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
print(run_cmd(sys.argv[1]))
|