gates / gates (push) Successful in 13m1s
Spike: per-app normal levels on three boxes (19-31 days), the off-site push as a line measure (a lower bound), qBittorrent/Transmission/Plex explainers measured on 9202, a tc ceiling measured (not built). Live on 9202 with 0.342.0-rc.1: limit set; real seed explained; CPU burner and the hidden flood alarmed once each and cleared once; the household's limit left alone. Teardown: apps removed, 9202 back on 0.341.0, receiver 9471 destroyed.
90 lines
6.2 KiB
Python
90 lines
6.2 KiB
Python
# Runs INSIDE guest 9202. Asks qBittorrent's own Web API and reads the container's exact byte counters.
|
|
# The admin password is read from the container env at use (as the controller does) and never printed.
|
|
import json, subprocess, sys, time, urllib.request, urllib.parse, http.client, socket, re, uuid
|
|
def sh(*a): return subprocess.run(a, capture_output=True, text=True).stdout.strip()
|
|
C = sys.argv[1] if len(sys.argv) > 1 else 'qbittorrent'
|
|
IP = sh('docker', 'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}', C).split()[0]
|
|
PID = sh('docker', 'inspect', '-f', '{{.State.Pid}}', C)
|
|
CID = sh('docker', 'inspect', '-f', '{{.Id}}', C)
|
|
BASE = f'http://{IP}:8080'
|
|
PW = sh('docker', 'exec', C, 'printenv', 'QBT_ADMIN_PASSWORD')
|
|
def req(method, path, data=None, hdr=None, raw=False):
|
|
h = {'Referer': BASE}
|
|
h.update(hdr or {})
|
|
if COOKIE: h['Cookie'] = COOKIE
|
|
r = urllib.request.Request(BASE + path, data=data, headers=h, method=method)
|
|
try:
|
|
with urllib.request.urlopen(r, timeout=20) as resp:
|
|
b = resp.read(); return resp.status, (b if raw else (json.loads(b) if b and b[:1] in b'[{' else b.decode()))
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, e.read().decode()[:200]
|
|
COOKIE = None
|
|
code, _ = None, None
|
|
r = urllib.request.Request(BASE + '/api/v2/auth/login', data=urllib.parse.urlencode({'username': 'admin', 'password': PW}).encode(),
|
|
headers={'Referer': BASE, 'Content-Type': 'application/x-www-form-urlencoded'}, method='POST')
|
|
with urllib.request.urlopen(r, timeout=20) as resp:
|
|
m = re.search(r'((?:QBT_)?SID[A-Za-z0-9_]*)=([^;]+)', '; '.join(resp.headers.get_all('Set-Cookie') or []))
|
|
print('login', resp.status, 'cookie' if m else 'NO COOKIE')
|
|
COOKIE = f'{m.group(1)}={m.group(2)}'
|
|
def netdev():
|
|
for line in open(f'/proc/{PID}/net/dev'):
|
|
if line.strip().startswith('eth0:'):
|
|
f = line.split(':', 1)[1].split(); return int(f[8])
|
|
def engine_stats():
|
|
s = socket.socket(socket.AF_UNIX); s.connect('/var/run/docker.sock')
|
|
s.sendall(f'GET /containers/{CID}/stats?stream=false&one-shot=true HTTP/1.0\r\n\r\n'.encode())
|
|
b = b''
|
|
while True:
|
|
x = s.recv(65536)
|
|
if not x: break
|
|
b += x
|
|
j = json.loads(b.split(b'\r\n\r\n', 1)[1])
|
|
return sum(n['tx_bytes'] for n in j.get('networks', {}).values()), j['cpu_stats']['cpu_usage']['total_usage']
|
|
def cli_stats():
|
|
return sh('docker', 'stats', '--no-stream', '--format', '{{.NetIO}}', C)
|
|
mode = sys.argv[2] if len(sys.argv) > 2 else 'info'
|
|
if mode == 'info':
|
|
print('version', req('GET', '/api/v2/app/version')[1], 'webapi', req('GET', '/api/v2/app/webapiVersion')[1])
|
|
print('transfer/info', req('GET', '/api/v2/transfer/info')[1])
|
|
p = req('GET', '/api/v2/app/preferences')[1]
|
|
print('prefs', {k: p.get(k) for k in ('up_limit', 'dl_limit', 'alt_up_limit', 'alt_dl_limit', 'scheduler_enabled', 'limit_tcp_overhead', 'limit_lan_peers', 'limit_utp_rate', 'max_uploads', 'enable_upload_suggestions')})
|
|
print('uploadLimit', req('GET', '/api/v2/transfer/uploadLimit')[1], 'speedLimitsMode', req('GET', '/api/v2/transfer/speedLimitsMode')[1])
|
|
elif mode == 'add':
|
|
bnd = uuid.uuid4().hex
|
|
tor = open('/root/uwtest.torrent', 'rb').read()
|
|
parts = []
|
|
for k, v in (('savepath', '/mnt/felhom-drives/scratch_hdd/userdata/media/torrents'), ('skip_checking', 'true'), ('category', '')):
|
|
parts.append(f'--{bnd}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode())
|
|
parts.append(f'--{bnd}\r\nContent-Disposition: form-data; name="torrents"; filename="uwtest.torrent"\r\nContent-Type: application/x-bittorrent\r\n\r\n'.encode() + tor + b'\r\n')
|
|
parts.append(f'--{bnd}--\r\n'.encode())
|
|
print('add', req('POST', '/api/v2/torrents/add', b''.join(parts), {'Content-Type': f'multipart/form-data; boundary={bnd}'}))
|
|
time.sleep(3)
|
|
print('torrents', [(t['name'], t['state'], t['progress']) for t in req('GET', '/api/v2/torrents/info')[1]])
|
|
elif mode == 'setlimit':
|
|
print('setUploadLimit', req('POST', '/api/v2/transfer/setUploadLimit', urllib.parse.urlencode({'limit': sys.argv[3]}).encode(), {'Content-Type': 'application/x-www-form-urlencoded'}))
|
|
print('uploadLimit now', req('GET', '/api/v2/transfer/uploadLimit')[1], 'prefs.up_limit', req('GET', '/api/v2/app/preferences')[1].get('up_limit'))
|
|
elif mode == 'setpref':
|
|
print('setPreferences', req('POST', '/api/v2/app/setPreferences', urllib.parse.urlencode({'json': json.dumps({'up_limit': int(sys.argv[3])})}).encode(), {'Content-Type': 'application/x-www-form-urlencoded'}))
|
|
print('uploadLimit now', req('GET', '/api/v2/transfer/uploadLimit')[1], 'prefs.up_limit', req('GET', '/api/v2/app/preferences')[1].get('up_limit'))
|
|
elif mode == 'peer':
|
|
print('addPeers', req('POST', '/api/v2/torrents/addPeers', urllib.parse.urlencode({'hashes': 'all', 'peers': sys.argv[3]}).encode(), {'Content-Type': 'application/x-www-form-urlencoded'}))
|
|
print('reannounce', req('POST', '/api/v2/torrents/reannounce', urllib.parse.urlencode({'hashes': 'all'}).encode(), {'Content-Type': 'application/x-www-form-urlencoded'}))
|
|
elif mode == 'post':
|
|
print(sys.argv[3], req('POST', sys.argv[3], urllib.parse.urlencode(json.loads(sys.argv[4])).encode(), {'Content-Type': 'application/x-www-form-urlencoded'}))
|
|
elif mode == 'sample':
|
|
dur, step = int(sys.argv[3]), int(sys.argv[4])
|
|
t0 = time.time(); prev = None
|
|
while time.time() - t0 <= dur:
|
|
ti = req('GET', '/api/v2/transfer/info')[1]
|
|
tor = req('GET', '/api/v2/torrents/info')[1]
|
|
etx, ecpu = engine_stats()
|
|
row = {'t': round(time.time() - t0, 1), 'qb_up_speed': ti['up_info_speed'], 'qb_up_data': ti['up_info_data'],
|
|
'qb_tor_uploaded': sum(x.get('uploaded', 0) for x in tor), 'proc_tx': netdev(), 'engine_tx': etx, 'cli': cli_stats()}
|
|
if prev:
|
|
dt = row['t'] - prev['t']
|
|
row['rate_qb_data_Mbit'] = round((row['qb_up_data'] - prev['qb_up_data']) * 8 / dt / 1e6, 2)
|
|
row['rate_proc_Mbit'] = round((row['proc_tx'] - prev['proc_tx']) * 8 / dt / 1e6, 2)
|
|
row['rate_engine_Mbit'] = round((row['engine_tx'] - prev['engine_tx']) * 8 / dt / 1e6, 2)
|
|
print(json.dumps(row), flush=True); prev = row
|
|
time.sleep(step)
|