feat(openwrt): add TollGate provision button and direct-download fallback

- OpenWrtGateway.vue: add "Install TollGate" button when not installed;
  tracks connected credentials for reuse in the provision call
- install.rs: fall back to wget download from GitHub releases when the
  package is not in any opkg feed (mips_24kc and other arches supported)
- openwrt.rs: provision-tollgate now falls back to saved router_config
  for credentials, matching the behaviour of get-status

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ssmithx 2026-06-29 01:22:45 +00:00
parent 6c534715ec
commit f054766a58
3 changed files with 99 additions and 12 deletions

View File

@ -136,23 +136,27 @@ impl RpcHandler {
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let p = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let saved = net_router::load_router_config(&self.config.data_dir).await?;
let p = params.unwrap_or_default();
let host = p
.get("host")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing host"))?
.to_string();
.map(|s| s.to_string())
.or_else(|| if saved.configured { Some(saved.address.clone()) } else { None })
.ok_or_else(|| anyhow::anyhow!("No router configured — provide host or call router.configure first"))?;
let ssh_user = p
.get("ssh_user")
.and_then(|v| v.as_str())
.unwrap_or("root")
.to_string();
.map(|s| s.to_string())
.or_else(|| saved.username.clone())
.unwrap_or_else(|| "root".to_string());
let ssh_password = p
.get("ssh_password")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
.map(|s| s.to_string())
.or_else(|| saved.password.clone())
.unwrap_or_default();
let default_mint_url = format!("http://{}:{}", self.config.host_ip, LOCAL_MINT_PORT);
let mint_url = p

View File

@ -6,11 +6,50 @@ use crate::Router;
/// The OpenWrt package name for the TollGate reference implementation.
const TOLLGATE_PACKAGE: &str = "tollgate-module-basic-go";
/// Install tollgate-module-basic-go via opkg.
/// Direct-download fallback URLs by opkg architecture string.
/// Used when the package is not in any configured feed.
/// Source: https://github.com/OpenTollGate/tollgate-module-basic-go/releases/tag/v0.2.0
fn ipk_url(arch: &str) -> Option<&'static str> {
match arch {
"mips_24kc" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/mips_24kc.ipk"),
"mipsel_24kc" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/mipsel_24kc.ipk"),
"aarch64_cortex-a53" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/aarch64_cortex-a53.ipk"),
"aarch64_cortex-a72" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/aarch64_cortex-a72.ipk"),
"arm_cortex-a7" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/arm_cortex-a7.ipk"),
_ => None,
}
}
/// Install tollgate-module-basic-go.
///
/// Tries opkg first (works if a custom feed is configured). Falls back to
/// downloading the .ipk directly from GitHub releases if opkg can't find it.
/// Caller is responsible for running `opkg_update` first.
pub fn install_tollgate(router: &Router) -> Result<()> {
info!("[{}] Installing {}", router.host, TOLLGATE_PACKAGE);
router.opkg_install(TOLLGATE_PACKAGE)?;
// Fast path: standard opkg install (or already installed).
if router.opkg_install(TOLLGATE_PACKAGE).is_ok() {
return Ok(());
}
// Package not in any feed — download the .ipk directly.
let arch = router
.run_ok("opkg print-architecture | grep -v 'all\\|noarch' | tail -1 | awk '{print $2}'")?;
let arch = arch.trim();
let url = ipk_url(arch).ok_or_else(|| {
anyhow::anyhow!(
"No pre-built TollGate package for architecture '{}'. \
Add a custom opkg feed or build from source.",
arch
)
})?;
info!("[{}] Downloading TollGate for {} from GitHub releases", router.host, arch);
router.run_ok(&format!("wget -O /tmp/tollgate.ipk '{}'", url))?;
router.run_ok("opkg install --force-depends /tmp/tollgate.ipk")?;
router.run_ok("rm -f /tmp/tollgate.ipk")?;
Ok(())
}

View File

@ -48,6 +48,13 @@ const sshPassword = ref('')
const showConnectForm = ref(false)
const connecting = ref(false)
// Credentials used for the last successful connection (reused for provisioning)
const connectedParams = ref<Record<string, string> | null>(null)
const provisioning = ref(false)
const provisionError = ref('')
const provisionSuccess = ref(false)
async function load(params?: Record<string, string>) {
loading.value = true
error.value = ''
@ -58,6 +65,7 @@ async function load(params?: Record<string, string>) {
timeout: 30000,
})
showConnectForm.value = false
if (params) connectedParams.value = params
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
if (msg.includes('No router configured')) {
@ -81,6 +89,28 @@ async function connect() {
}
}
async function provisionTollgate() {
provisioning.value = true
provisionError.value = ''
provisionSuccess.value = false
try {
const params: Record<string, unknown> = {
// Use explicitly connected creds if available, otherwise fall back to
// host from the loaded status (backend will use saved router_config).
host: connectedParams.value?.host ?? status.value?.host,
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
}
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 180000 })
provisionSuccess.value = true
await load(connectedParams.value ?? undefined)
} catch (e) {
provisionError.value = e instanceof Error ? e.message : String(e)
} finally {
provisioning.value = false
}
}
function formatUptime(secs: number): string {
const d = Math.floor(secs / 86400)
const h = Math.floor((secs % 86400) / 3600)
@ -229,9 +259,23 @@ onMounted(() => load())
<div class="rounded-xl border border-white/10 bg-white/5 p-5 mb-4">
<h2 class="text-sm font-semibold text-white/80 mb-4">TollGate</h2>
<div v-if="!status.tollgate.installed" class="flex items-center gap-3">
<span class="w-2 h-2 rounded-full bg-white/20 inline-block"></span>
<span class="text-sm text-white/50">Not installed</span>
<div v-if="!status.tollgate.installed">
<div class="flex items-center gap-3 mb-4">
<span class="w-2 h-2 rounded-full bg-white/20 inline-block"></span>
<span class="text-sm text-white/50">Not installed</span>
</div>
<button
:disabled="provisioning"
class="w-full py-2 rounded-lg text-sm font-medium transition-colors"
:class="provisioning
? 'bg-white/5 text-white/30 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-500 text-white'"
@click="provisionTollgate"
>
{{ provisioning ? 'Installing TollGate… (may take a few minutes)' : 'Install TollGate' }}
</button>
<p v-if="provisionError" class="mt-2 text-xs text-red-400">{{ provisionError }}</p>
<p v-if="provisionSuccess && !provisioning" class="mt-2 text-xs text-green-400">TollGate provisioned successfully.</p>
</div>
<template v-else>