routers: add optional SSH port

Add an optional SSH port field in the routers UI and use it for push_all SSH connections.

Made-with: Cursor
This commit is contained in:
Андрей Бобырев
2026-04-29 16:23:11 +03:00
parent 702d121c8a
commit 6761df00ca
2 changed files with 31 additions and 2 deletions

View File

@@ -65,6 +65,13 @@ async def _ssh_on_router(rcfg: dict, remote_cmd: str, timeout: int = 45) -> tupl
return 1, "", "нет IP и нет тоннеля (добавь IP или настрой тоннель)" return 1, "", "нет IP и нет тоннеля (добавь IP или настрой тоннель)"
ssh_host = ip ssh_host = ip
extra_args = [] extra_args = []
# Optional direct SSH port (default: 22). Tunnel uses its own port args above.
try:
p = int(rcfg.get("ssh_port") or 22)
if p and p != 22:
extra_args = ["-p", str(p)]
except Exception:
pass
user = rcfg.get("user") or config.SSH_USER user = rcfg.get("user") or config.SSH_USER
pwd = rcfg.get("password") or config.SSH_PASS pwd = rcfg.get("password") or config.SSH_PASS
try: try:
@@ -103,8 +110,16 @@ async def _push_one_router(server_url: str, router_key: str, rcfg: dict) -> dict
"-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10",
"-p", str(int(tunnel_port)), f"{user}@127.0.0.1", cmd] "-p", str(int(tunnel_port)), f"{user}@127.0.0.1", cmd]
else: else:
port_args: list[str] = []
try:
p = int(rcfg.get("ssh_port") or 22)
if p and p != 22:
port_args = ["-p", str(p)]
except Exception:
port_args = []
ssh_cmd = ["sshpass", "-p", pwd, "ssh", ssh_cmd = ["sshpass", "-p", pwd, "ssh",
"-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10",
*port_args,
f"{user}@{ip}", cmd] f"{user}@{ip}", cmd]
try: try:
r = await asyncio.to_thread(subprocess.run, ssh_cmd, capture_output=True, text=True, timeout=60) r = await asyncio.to_thread(subprocess.run, ssh_cmd, capture_output=True, text=True, timeout=60)

View File

@@ -207,6 +207,7 @@ input[type=text]:focus,input[type=password]:focus{border-color:var(--accent)}
<input type="text" id="r-name" placeholder="Имя (andrey)" style="width:130px"> <input type="text" id="r-name" placeholder="Имя (andrey)" style="width:130px">
<input type="text" id="r-ip" placeholder="IP или hostname SSH" style="min-width:160px;flex:1;max-width:240px"> <input type="text" id="r-ip" placeholder="IP или hostname SSH" style="min-width:160px;flex:1;max-width:240px">
<input type="text" id="r-user" value="root" style="width:80px"> <input type="text" id="r-user" value="root" style="width:80px">
<input type="text" id="r-port" placeholder="SSH порт (опц.)" inputmode="numeric" style="width:120px">
<input type="password" id="r-pass" placeholder="SSH пароль" style="width:130px"> <input type="password" id="r-pass" placeholder="SSH пароль" style="width:130px">
<button class="btn btn-b" onclick="addRouter()">+ Добавить</button> <button class="btn btn-b" onclick="addRouter()">+ Добавить</button>
<button class="btn" style="background:var(--orange);color:#000" onclick="addAndTunnel()" title="Добавить роутер без IP и сразу открыть настройку тоннеля">⇄ Тоннель</button> <button class="btn" style="background:var(--orange);color:#000" onclick="addAndTunnel()" title="Добавить роутер без IP и сразу открыть настройку тоннеля">⇄ Тоннель</button>
@@ -430,10 +431,17 @@ async function loadRouters(){
const R=await(await fetch('/api/routers')).json(); const R=await(await fetch('/api/routers')).json();
const el=document.getElementById('routers-list'); const el=document.getElementById('routers-list');
if(!Object.keys(R).length){ el.innerHTML='<span style="color:var(--muted)">Роутеры не добавлены</span>'; return; } if(!Object.keys(R).length){ el.innerHTML='<span style="color:var(--muted)">Роутеры не добавлены</span>'; return; }
const _hostWithPort = (c) => {
const host=(c.ip||'').toString().trim();
const p=(c.ssh_port!=null && c.ssh_port!=='' && !Number.isNaN(Number(c.ssh_port))) ? Number(c.ssh_port) : null;
if(!host) return '';
if(!p) return host;
return `${host}:${p}`;
};
el.innerHTML=Object.entries(R).map(([n,c])=>{ el.innerHTML=Object.entries(R).map(([n,c])=>{
const enc=encodeURIComponent(n); const enc=encodeURIComponent(n);
return `<div class="ri" style="align-items:flex-start;gap:10px"> return `<div class="ri" style="align-items:flex-start;gap:10px">
<div style="flex:1;min-width:0"><span class="n">${_escHtml(n)}</span> <span style="font-size:12px">${c.tunnel_port?`<span style="color:var(--green)">⇄ тоннель :${c.tunnel_port}</span>`:`<span style="color:var(--muted)">— ${_escHtml(c.ip||'—')}</span>`}</span></div> <div style="flex:1;min-width:0"><span class="n">${_escHtml(n)}</span> <span style="font-size:12px">${c.tunnel_port?`<span style="color:var(--green)">⇄ тоннель :${c.tunnel_port}</span>`:`<span style="color:var(--muted)">— ${_escHtml(_hostWithPort(c)||'—')}</span>`}</span></div>
<div style="display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end"> <div style="display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end">
<button type="button" class="btn btn-ghost" style="font-size:11px;padding:5px 10px" data-r-act="test" data-router="${enc}">🔌 Тест</button> <button type="button" class="btn btn-ghost" style="font-size:11px;padding:5px 10px" data-r-act="test" data-router="${enc}">🔌 Тест</button>
<button type="button" class="btn btn-b" style="font-size:11px;padding:5px 10px" data-r-act="pull" data-router="${enc}">⬇ С роутера</button> <button type="button" class="btn btn-b" style="font-size:11px;padding:5px 10px" data-r-act="pull" data-router="${enc}">⬇ С роутера</button>
@@ -443,7 +451,13 @@ async function loadRouters(){
</div></div>`; </div></div>`;
}).join(''); }).join('');
} }
async function addRouter(){ const n=document.getElementById('r-name').value.trim(); if(!n)return; const b={ip:document.getElementById('r-ip').value.trim(),user:document.getElementById('r-user').value||'root',password:document.getElementById('r-pass').value}; try{ await fetch('/api/routers/'+encodeURIComponent(n),{method:'POST',headers:authHdr(),body:JSON.stringify(b)}); sm('rm','ok','✅ '+n); loadRouters(); }catch(e){sm('rm','err','❌ '+e)} } async function addRouter(){
const n=document.getElementById('r-name').value.trim(); if(!n)return;
const portRaw=(document.getElementById('r-port')?.value||'').trim();
const p = portRaw ? Number(portRaw) : null;
const ssh_port = (p && Number.isFinite(p) && p > 0) ? Math.trunc(p) : null;
const b={ip:document.getElementById('r-ip').value.trim(),user:document.getElementById('r-user').value||'root',ssh_port:ssh_port,password:document.getElementById('r-pass').value};
try{ await fetch('/api/routers/'+encodeURIComponent(n),{method:'POST',headers:authHdr(),body:JSON.stringify(b)}); sm('rm','ok','✅ '+n); loadRouters(); }catch(e){sm('rm','err','❌ '+e)} }
async function addAndTunnel(){ async function addAndTunnel(){
const n=document.getElementById('r-name').value.trim(); const n=document.getElementById('r-name').value.trim();
if(!n){sm('rm','err','❌ Введи имя роутера');return;} if(!n){sm('rm','err','❌ Введи имя роутера');return;}