fix remote ubuntu server

This commit is contained in:
2026-06-05 14:56:03 +07:00
parent 4b65838f0f
commit 08b94337ad
5 changed files with 130 additions and 47 deletions

View File

@@ -63,12 +63,21 @@ def get_settings() -> Settings:
robot_package_base_url = os.getenv("ROBOT_PACKAGE_BASE_URL", "https://package.pnkr.cloud").rstrip("/")
return Settings(
agent_version=os.getenv("AGENT_VERSION", "1.0.0"),
host=os.getenv("AGENT_HOST", "127.0.0.1"),
host=os.getenv("AGENT_HOST", "0.0.0.0"),
port=int(os.getenv("AGENT_PORT", "5010")),
robot_package_base_url=robot_package_base_url,
allowed_origins=_csv(
os.getenv("ALLOWED_ORIGINS"),
["https://app.pnkr.cloud", "https://package.pnkr.cloud", "http://localhost:3000", "http://localhost:5173"],
[
"https://app.pnkr.cloud",
"https://package.pnkr.cloud",
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:8080",
"http://127.0.0.1:8080",
],
),
allowed_download_hosts=_csv(
os.getenv("ALLOWED_DOWNLOAD_HOSTS"),

View File

@@ -4,7 +4,7 @@ After=network.target
[Service]
WorkingDirectory=/opt/local-installer-agent
ExecStart=/opt/local-installer-agent/venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port 5010
ExecStart=/opt/local-installer-agent/venv/bin/python -m uvicorn app.main:app --host ${AGENT_HOST} --port ${AGENT_PORT}
Restart=always
User=root
EnvironmentFile=/etc/local-installer-agent/agent.env

View File

@@ -3,6 +3,8 @@ set -euo pipefail
VERSION="${VERSION:-1.0.0}"
ARCH="${ARCH:-amd64}"
AGENT_HOST="${AGENT_HOST:-0.0.0.0}"
AGENT_PORT="${AGENT_PORT:-5010}"
DEB_COMPRESSION="${DEB_COMPRESSION:-gzip}"
PKG_NAME="local-installer-agent"
BUILD_ROOT="${BUILD_ROOT:-build}"
@@ -53,10 +55,10 @@ chmod 755 "${BUILD_DIR}/DEBIAN"
cat > "${BUILD_DIR}/etc/local-installer-agent/agent.env" <<EOF
AGENT_VERSION=${VERSION}
AGENT_HOST=127.0.0.1
AGENT_PORT=5010
AGENT_HOST=${AGENT_HOST}
AGENT_PORT=${AGENT_PORT}
ROBOT_PACKAGE_BASE_URL=https://package.pnkr.cloud
ALLOWED_ORIGINS=https://app.pnkr.cloud,https://package.pnkr.cloud,http://localhost:3000,http://localhost:5173
ALLOWED_ORIGINS=https://app.pnkr.cloud,https://package.pnkr.cloud,http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173,http://localhost:8080,http://127.0.0.1:8080
ALLOWED_DOWNLOAD_HOSTS=package.pnkr.cloud
ALLOWED_DOCKER_REGISTRIES=registry.robot.package,docker.io
CACHE_DIR=/var/cache/local-installer-agent/packages

View File

@@ -169,6 +169,50 @@ function getClientOsLabel(os) {
return CLIENT_OS_LABELS[os] || CLIENT_OS_LABELS.unknown;
}
function parseUrlLike(value) {
const text = normalizeUrl(value);
if (!text) return null;
const urlText = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(text)
? text
: `http://${text}`;
try {
return new URL(urlText);
} catch {
return null;
}
}
function isLoopbackHostname(hostname) {
const host = String(hostname || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
return Boolean(
host === 'localhost'
|| host.endsWith('.localhost')
|| host === '::1'
|| host === '0.0.0.0'
|| /^127(?:\.\d{1,3}){0,3}$/.test(host)
);
}
function isLoopbackAgentEndpoint(agentBaseUrl) {
const parsed = parseUrlLike(agentBaseUrl);
return !parsed?.hostname || isLoopbackHostname(parsed.hostname);
}
function resolveTargetOpenUrl(openUrl, agentBaseUrl, isRemoteAgentEndpoint) {
if (!isRemoteAgentEndpoint) return openUrl;
const parsedOpenUrl = parseUrlLike(openUrl);
const parsedAgentUrl = parseUrlLike(agentBaseUrl);
if (!parsedOpenUrl || !parsedAgentUrl || !isLoopbackHostname(parsedOpenUrl.hostname)) {
return openUrl;
}
parsedOpenUrl.hostname = parsedAgentUrl.hostname;
return parsedOpenUrl.toString();
}
function App() {
const [settings, setSettings] = useState(readSettings);
const [draftSettings, setDraftSettings] = useState(settings);
@@ -197,8 +241,14 @@ function App() {
const clientOs = useMemo(() => detectClientOs(), []);
const isClientLinux = clientOs === 'linux';
const isClientWindows = clientOs === 'windows';
const canShowAgentCommand = isClientLinux || isClientWindows;
const isLocalAgentEndpoint = useMemo(() => isLoopbackAgentEndpoint(agentBaseUrl), [agentBaseUrl]);
const isRemoteAgentEndpoint = !isLocalAgentEndpoint;
const canUseAgentEndpoint = isRemoteAgentEndpoint || isClientLinux;
const canManageApps = canUseAgentEndpoint;
const canShowAgentCommand = isClientLinux || isClientWindows || isRemoteAgentEndpoint;
const clientOsLabel = getClientOsLabel(clientOs);
const agentTargetLabel = isRemoteAgentEndpoint ? 'Remote Ubuntu Agent' : 'Local Agent';
const agentTargetMachine = isRemoteAgentEndpoint ? 'remote Ubuntu server' : 'local machine';
const installedByAppId = useMemo(() => {
return new Map(installedApps.map((app) => [app.appId, app]));
@@ -284,24 +334,24 @@ function App() {
}, [packageBaseUrl]);
const refreshAgent = useCallback(async () => {
if (!isClientLinux) {
if (!canUseAgentEndpoint) {
setAgentHealth(null);
setSystemInfo(null);
setInstalledApps([]);
setAgentStatus({
state: 'warning',
message: isClientWindows
? 'Windows detected. Copy the Agent command and run it in Ubuntu SSH.'
: `Current client is ${clientOsLabel}. Web Client only supports Linux.`
? 'Local endpoint points to this Windows machine. Use a remote Ubuntu Agent endpoint.'
: `Local endpoint requires Linux. Current client is ${clientOsLabel}.`
});
return false;
}
setAgentStatus({ state: 'loading', message: 'Đang kiểm tra Agent local' });
setAgentStatus({ state: 'loading', message: `Checking ${agentTargetLabel}` });
try {
const health = await fetchAgentHealth(agentBaseUrl);
setAgentHealth(health);
setAgentStatus({ state: 'success', message: `${health.hostname || 'Agent'} online` });
setAgentStatus({ state: 'success', message: `${health.hostname || agentTargetLabel} online` });
const [info, installed] = await Promise.all([
fetchAgentSystemInfo(agentBaseUrl).catch(() => null),
@@ -317,7 +367,7 @@ function App() {
setAgentStatus({ state: 'danger', message: getErrorMessage(error) });
return false;
}
}, [agentBaseUrl, clientOsLabel, isClientLinux, isClientWindows]);
}, [agentBaseUrl, agentTargetLabel, canUseAgentEndpoint, clientOsLabel, isClientWindows]);
const refreshAll = useCallback(async () => {
await Promise.all([refreshPackage(), refreshAgent()]);
@@ -431,17 +481,17 @@ function App() {
}, []);
const runAppAction = useCallback(async (action, app) => {
if (!isClientLinux) {
notify('warning', 'Web Client chỉ hỗ trợ cài đặt trên máy Linux.');
if (!canManageApps) {
notify('warning', 'Local Agent endpoint can install only from a Linux browser machine. Use a remote Ubuntu Agent endpoint.');
return;
}
if (!agentHealth) {
notify('warning', 'Agent local đang offline. Cài Agent rồi bấm Retry.');
notify('warning', `${agentTargetLabel} is offline. Check the endpoint and press Retry.`);
return;
}
if (action === 'remove' && !window.confirm(`Remove ${app.appName} khỏi máy local?`)) {
if (action === 'remove' && !window.confirm(`Remove ${app.appName} from ${agentTargetMachine}?`)) {
return;
}
@@ -476,7 +526,7 @@ function App() {
} finally {
setBusyAction('');
}
}, [agentBaseUrl, agentHealth, isClientLinux, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
}, [agentBaseUrl, agentHealth, agentTargetLabel, agentTargetMachine, canManageApps, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
const openEndpointDialog = useCallback(() => {
setDraftSettings(settings);
@@ -502,7 +552,7 @@ function App() {
const copyInstallCommand = useCallback(async () => {
if (!canShowAgentCommand) {
notify('warning', 'Lệnh cài Agent chỉ hiển thị trên máy Linux.');
notify('warning', 'Agent command is hidden for this client OS.');
return;
}
@@ -590,6 +640,19 @@ function App() {
return () => window.removeEventListener('keydown', onKeyDown);
}, [closeEndpointDialog, endpointDialogOpen]);
const agentStatusTitle = !canUseAgentEndpoint
? (isClientWindows ? 'Remote endpoint needed' : 'Linux client required')
: (agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline'));
const agentStatusDetail = !canUseAgentEndpoint
? (isClientWindows ? 'Change endpoint to Ubuntu server IP' : `${clientOsLabel} detected`)
: (agentHealth ? `${agentHealth.hostname || agentTargetLabel} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : agentBaseUrl);
const agentStatusTone = !canUseAgentEndpoint
? 'warning'
: (agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : (agentStatus.state === 'loading' ? 'info' : 'danger')));
const topbarAgentState = !canUseAgentEndpoint
? 'Use Ubuntu Agent endpoint'
: (agentHealth ? `Ready on ${agentTargetMachine}` : `Waiting for ${agentTargetLabel}`);
return (
<div className="app-shell">
<aside className="sidebar">
@@ -607,9 +670,9 @@ function App() {
<div className="nav-label">Runtime</div>
<StatusBox
icon={agentHealth ? PlugZap : WifiOff}
title={!isClientLinux ? (isClientWindows ? 'SSH install mode' : 'Linux client required') : (agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline'))}
detail={!isClientLinux ? (isClientWindows ? 'Windows detected · run command on Ubuntu SSH' : `${clientOsLabel} detected`) : (agentHealth ? `${agentHealth.hostname || 'localhost'} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : '127.0.0.1:5010')}
tone={!isClientLinux ? 'warning' : (agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : 'danger'))}
title={agentStatusTitle}
detail={agentStatusDetail}
tone={agentStatusTone}
/>
<StatusBox
icon={Server}
@@ -625,7 +688,7 @@ function App() {
<strong title={packageBaseUrl}>{packageBaseUrl}</strong>
</div>
<div className="endpoint-summary-row">
<span>Local Agent</span>
<span>Agent endpoint</span>
<strong title={agentBaseUrl}>{agentBaseUrl}</strong>
</div>
</div>
@@ -640,7 +703,7 @@ function App() {
<header className="topbar">
<div className="topbar-title">
<span>robot.installer</span>
<strong>{!isClientLinux ? (isClientWindows ? 'Copy command for Ubuntu SSH' : 'Linux client required') : (agentHealth ? 'Ready for install' : 'Waiting for Agent')}</strong>
<strong>{topbarAgentState}</strong>
</div>
<div className="topbar-actions">
<a className="icon-button" href={joinUrl(packageBaseUrl, '/api/apps')} target="_blank" rel="noreferrer" title="Open package API">
@@ -657,7 +720,7 @@ function App() {
<div className="page-header">
<div>
<h1>Application catalog</h1>
<p>Released apps từ package server trạng thái cài đặt trên máy local.</p>
<p>Released apps from package server and install state on the selected Agent endpoint.</p>
</div>
{canShowAgentCommand && (
<div className="page-actions">
@@ -665,7 +728,7 @@ function App() {
<Clipboard size={15} aria-hidden="true" />
{agentNeedsUpdate ? 'Copy Agent update' : 'Copy Agent command'}
</button>
{isClientLinux && (
{isClientLinux && isLocalAgentEndpoint && (
<a className="btn btn-primary" href={joinUrl(packageBaseUrl, '/install-agent.sh')} target="_blank" rel="noreferrer">
<Download size={15} aria-hidden="true" />
{agentNeedsUpdate ? 'update-agent.sh' : 'install-agent.sh'}
@@ -677,12 +740,12 @@ function App() {
<div className="dashboard-stats">
<MetricCard label="Released apps" value={stats.available} note={packageStatus.state === 'loading' ? 'loading' : 'from API'} />
<MetricCard label="Installed local" value={stats.installed} note={agentHealth ? 'Agent SQLite' : 'Agent offline'} />
<MetricCard label="Installed target" value={stats.installed} note={agentHealth ? 'Agent SQLite' : 'Agent offline'} />
<MetricCard label="Updates" value={stats.updates} note="version diff" tone={stats.updates ? 'warning' : 'success'} />
<MetricCard label="Components" value={stats.components} note={selectedApp?.appCode || selectedApp?.appId || 'selected app'} />
</div>
{!isClientLinux && (
{!canUseAgentEndpoint && (
<ClientOsNotice
agentCommand={agentCommand}
canShowCommand={canShowAgentCommand}
@@ -692,11 +755,11 @@ function App() {
/>
)}
{isClientLinux && !agentHealth && (
{canUseAgentEndpoint && !agentHealth && (
<div className="offline-banner">
<AlertCircle size={19} aria-hidden="true" />
<div>
<strong>Local Installer Agent chưa online</strong>
<strong>{agentTargetLabel} is offline</strong>
<code>{agentCommand}</code>
</div>
<button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
@@ -706,11 +769,11 @@ function App() {
</div>
)}
{isClientLinux && agentNeedsUpdate && (
{canUseAgentEndpoint && agentNeedsUpdate && (
<div className="offline-banner agent-update-banner">
<AlertCircle size={19} aria-hidden="true" />
<div>
<strong>Agent {latestAgentPackage.version} đã sẵn sàng</strong>
<strong>Agent {latestAgentPackage.version} is ready</strong>
<code>{agentCommand}</code>
</div>
<button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
@@ -778,9 +841,10 @@ function App() {
const installBusy = busyAction === `install:${app.appId}` || (rowTaskBusy && rowTask.action === 'install');
const updateBusy = busyAction === `update:${app.appId}` || (rowTaskBusy && rowTask.action === 'update');
const removeBusy = busyAction === `remove:${app.appId}` || (rowTaskBusy && rowTask.action === 'remove');
const openUrl = app.installed
const appOpenUrl = app.installed
? getAppOpenUrl({ ...app, openUrl: app.openUrl || app.installed.openUrl })
: '';
const openUrl = resolveTargetOpenUrl(appOpenUrl, agentBaseUrl, isRemoteAgentEndpoint);
return (
<tr
@@ -803,7 +867,7 @@ function App() {
<td>{app.packageCount || 0}</td>
<td className="action-col">
<div className="action-group">
{isClientLinux && !app.installed && (
{canManageApps && !app.installed && (
<button
className="btn btn-primary compact"
type="button"
@@ -817,7 +881,7 @@ function App() {
Install
</button>
)}
{isClientLinux && app.installed && app.canUpdate && (
{canManageApps && app.installed && app.canUpdate && (
<button
className="btn btn-warning compact"
type="button"
@@ -831,7 +895,7 @@ function App() {
Update
</button>
)}
{isClientLinux && app.installed && openUrl && (
{canManageApps && app.installed && openUrl && (
<a
className="btn btn-secondary compact"
href={openUrl}
@@ -844,7 +908,7 @@ function App() {
Open App
</a>
)}
{isClientLinux && app.installed && (
{canManageApps && app.installed && (
<button
className="icon-button danger"
type="button"
@@ -877,10 +941,12 @@ function App() {
health={agentHealth}
systemInfo={systemInfo}
status={agentStatus}
title={agentTargetLabel}
endpoint={agentBaseUrl}
latestAgentPackage={latestAgentPackage}
needsUpdate={agentNeedsUpdate}
onCopyUpdate={copyInstallCommand}
showAgentActions={isClientLinux}
showAgentActions={canShowAgentCommand}
/>
{activeTask && (
<TaskPanel
@@ -952,7 +1018,7 @@ function EndpointDialog({ draftSettings, onApply, onCancel, onChange }) {
/>
</label>
<label className="settings-field">
<span>Local Agent</span>
<span>Agent endpoint</span>
<input
value={draftSettings.agentBaseUrl}
onChange={(event) => onChange((current) => ({
@@ -1007,11 +1073,11 @@ function ClientOsNotice({ agentCommand, canShowCommand, isWindows, onCopyCommand
<div className={`offline-banner client-os-banner ${canShowCommand ? 'command-visible' : ''}`}>
<AlertCircle size={19} aria-hidden="true" />
<div>
<strong>{isWindows ? 'Cài Agent qua Ubuntu SSH' : 'Web Client chỉ hỗ trợ máy Linux'}</strong>
<strong>{isWindows ? 'Remote Ubuntu endpoint needed' : 'Local Agent requires Linux'}</strong>
<p>
{isWindows
? 'Máy hiện tại là Windows. Hãy SSH vào Ubuntu server rồi chạy lệnh bên dưới trong terminal Ubuntu.'
: `Máy hiện tại được nhận diện là ${osLabel}. Lệnh cài Agent và các thao tác cài đặt đã được ẩn.`}
? 'The endpoint is localhost, so it points at this Windows machine. Change it to http://<ubuntu-server-ip>:5010 or run the command below over Ubuntu SSH.'
: `Current client is ${osLabel}. Use a remote Ubuntu Agent endpoint or open the Web Client from Linux.`}
</p>
{canShowCommand && <code>{agentCommand}</code>}
</div>
@@ -1147,13 +1213,13 @@ function TaskPanel({ task, onClear, onRefresh }) {
);
}
function AgentPanel({ health, systemInfo, status, latestAgentPackage, needsUpdate, onCopyUpdate, showAgentActions }) {
function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPackage, needsUpdate, onCopyUpdate, showAgentActions }) {
return (
<section className="panel">
<div className="panel-header">
<div>
<h2>Local Agent</h2>
<p>{status.message || '127.0.0.1:5010'}</p>
<h2>{title || 'Agent'}</h2>
<p>{status.message || endpoint || '127.0.0.1:5010'}</p>
</div>
{needsUpdate ? <AlertCircle className="panel-state warning" size={20} aria-hidden="true" /> : health ? <CheckCircle2 className="panel-state success" size={20} aria-hidden="true" /> : <XCircle className="panel-state danger" size={20} aria-hidden="true" />}
</div>

View File

@@ -1422,7 +1422,9 @@ app.get('/install-agent.sh', (req, res) => {
baseUrl,
...publicApiCorsOrigins.filter((origin) => origin !== '*'),
'http://localhost:3000',
'http://127.0.0.1:3000'
'http://127.0.0.1:3000',
'http://localhost:8080',
'http://127.0.0.1:8080'
])).join(',');
res.type('text/x-shellscript').send(`#!/usr/bin/env bash
set -euo pipefail
@@ -1434,6 +1436,8 @@ AGENT_ENV="/etc/local-installer-agent/agent.env"
PACKAGE_HOST="$(printf '%s' "$PACKAGE_BASE_URL" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://([^/:]+).*$#\\1#')"
PACKAGE_REGISTRY="$(printf '%s' "$PACKAGE_BASE_URL" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://([^/]+).*$#\\1#')"
TMP_DEB="/tmp/local-installer-agent.deb"
AGENT_BIND_HOST="\${AGENT_HOST:-0.0.0.0}"
AGENT_BIND_PORT="\${AGENT_PORT:-5010}"
set_agent_env() {
KEY="$1"
@@ -1455,6 +1459,8 @@ apt install -y "$TMP_DEB"
echo "Configuring Local Installer Agent..."
mkdir -p /etc/local-installer-agent
touch "$AGENT_ENV"
set_agent_env AGENT_HOST "$AGENT_BIND_HOST"
set_agent_env AGENT_PORT "$AGENT_BIND_PORT"
set_agent_env ROBOT_PACKAGE_BASE_URL "$PACKAGE_BASE_URL"
set_agent_env ALLOWED_ORIGINS "${escapeShellDoubleQuoted(agentAllowedOrigins)}"
set_agent_env ALLOWED_DOWNLOAD_HOSTS "$PACKAGE_HOST,localhost,127.0.0.1"
@@ -1468,7 +1474,7 @@ systemctl restart local-installer-agent
echo "Checking Agent..."
for attempt in $(seq 1 20); do
if curl -fsSL http://127.0.0.1:5010/health; then
if curl -fsSL "http://127.0.0.1:$AGENT_BIND_PORT/health"; then
echo ""
echo "Local Installer Agent installed successfully."
exit 0