vdi-cloud/tools/pve_term_exec.py

90 lines
2.8 KiB
Python
Raw Normal View History

"""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]))