mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 02:45:13 +08:00
💋 Sync 2026-10-04 04:16:09
This commit is contained in:
1 parent
f6b2df73ec
commit
f62992fd80
745 files changed
+212416
-1173
No files matched your search
@@ -0,0 +1,3 @@
|
||||
config luci_podman 'globals'
|
||||
option init_start_priority '100'
|
||||
option socket_path 'unix:///run/podman/podman.sock'
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Handle config: podman-opkg is the shipped default, podman is the live config
|
||||
if [ ! -f /etc/config/luci-podman ]; then
|
||||
mv /etc/config/luci-podman-opkg /etc/config/luci-podman
|
||||
else
|
||||
echo "Existing /etc/config/luci-podman preserved. New defaults at /etc/config/luci-podman-opkg"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { popen, writefile, unlink } from 'fs';
|
||||
import * as podman_socket from 'luci.podman_socket'; // ucode-lsp disable
|
||||
import { validate_int, validate_name, require_param } from 'luci.podman_validate'; // ucode-lsp disable
|
||||
|
||||
function die(msg) {
|
||||
warn(`${msg}\n`);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Volume export/import here shells out to the local `podman` CLI, which can
|
||||
// only reach a Podman daemon running on the same host. When the user has
|
||||
// configured a remote TCP Podman, refuse upfront rather than failing weirdly.
|
||||
if (podman_socket.is_remote())
|
||||
die(`Volume operations are not supported with a remote Podman (${podman_socket.get_dest()}); the CLI helper requires a local daemon.`);
|
||||
|
||||
// --- Main ---
|
||||
|
||||
let action = ARGV[0];
|
||||
|
||||
if (action === 'volume_export') {
|
||||
let volumeName = ARGV[1];
|
||||
let err = require_param('volumeName', volumeName) || validate_name(volumeName);
|
||||
if (err) die(err);
|
||||
|
||||
// volume export outputs a tar stream - use podman CLI directly
|
||||
let escaped = replace(volumeName, /'/g, "'\\''");
|
||||
let p = popen(`/usr/bin/podman volume export '${escaped}'`, 'r');
|
||||
if (!p)
|
||||
die('Failed to run podman volume export');
|
||||
|
||||
let chunk;
|
||||
while ((chunk = p.read(65536)) != null && length(chunk) > 0)
|
||||
print(chunk);
|
||||
p.close();
|
||||
}
|
||||
else if (action === 'volume_import') {
|
||||
let volumeName = ARGV[1];
|
||||
let compressed = ARGV[2];
|
||||
let volumeData = ARGV[3];
|
||||
|
||||
let err = require_param('volumeName', volumeName) || validate_name(volumeName)
|
||||
|| require_param('volumeData', volumeData)
|
||||
|| require_param('compressed', compressed) || validate_int(compressed);
|
||||
if (err) die(err);
|
||||
|
||||
// Validate base64 content
|
||||
if (!match(volumeData, /^[A-Za-z0-9+\/]*={0,2}$/) || (length(volumeData) % 4) !== 0)
|
||||
die('Invalid Base64 content');
|
||||
|
||||
let decoded = b64dec(volumeData);
|
||||
if (decoded == null)
|
||||
die('Failed to decode import data');
|
||||
|
||||
// Decompress if needed
|
||||
if (compressed === '1') {
|
||||
let tp = popen('mktemp /tmp/podman_import_XXXXXX', 'r');
|
||||
let tmppath = tp ? trim(tp.read('all') || '') : '';
|
||||
if (tp) tp.close();
|
||||
if (!tmppath)
|
||||
die('Failed to create temp file');
|
||||
|
||||
writefile(tmppath, decoded);
|
||||
|
||||
let p = popen(`gunzip -c '${tmppath}'`, 'r');
|
||||
decoded = p ? p.read('all') : null;
|
||||
if (p) p.close();
|
||||
unlink(tmppath);
|
||||
|
||||
if (!decoded)
|
||||
die('Failed to decompress import data');
|
||||
}
|
||||
|
||||
// Ensure volume exists
|
||||
let escaped = replace(volumeName, /'/g, "'\\''");
|
||||
let p = popen(`/usr/bin/podman volume exists '${escaped}' 2>/dev/null; echo $?`, 'r');
|
||||
let rc = p ? trim(p.read('all') || '') : '';
|
||||
if (p) p.close();
|
||||
|
||||
if (rc !== '0') {
|
||||
p = popen(`/usr/bin/podman volume create '${escaped}' 2>/dev/null`, 'r');
|
||||
if (p) { p.read('all'); p.close(); }
|
||||
}
|
||||
|
||||
// Import via stdin pipe
|
||||
p = popen(`/usr/bin/podman volume import '${escaped}' -`, 'w');
|
||||
if (!p)
|
||||
die('Failed to start volume import');
|
||||
p.write(decoded);
|
||||
let exit_code = p.close();
|
||||
|
||||
if (exit_code) {
|
||||
// Cleanup on failure
|
||||
popen(`/usr/bin/podman volume rm '${escaped}' 2>/dev/null`, 'r')?.close();
|
||||
die('Import failed');
|
||||
}
|
||||
}
|
||||
else {
|
||||
die(`Unknown action: ${action}`);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"admin/podman": {
|
||||
"title": "Podman",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-podman"
|
||||
]
|
||||
}
|
||||
},
|
||||
"admin/podman/overview": {
|
||||
"title": "Overview",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/overview"
|
||||
}
|
||||
},
|
||||
"admin/podman/containers": {
|
||||
"title": "Containers",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/containers"
|
||||
}
|
||||
},
|
||||
"admin/podman/container/*": {
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/container"
|
||||
}
|
||||
},
|
||||
"admin/podman/pods": {
|
||||
"title": "Pods",
|
||||
"order": 25,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/pods"
|
||||
}
|
||||
},
|
||||
"admin/podman/pod/*": {
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/pod"
|
||||
}
|
||||
},
|
||||
"admin/podman/images": {
|
||||
"title": "Images",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/images"
|
||||
}
|
||||
},
|
||||
"admin/podman/networks": {
|
||||
"title": "Networks",
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/networks"
|
||||
}
|
||||
},
|
||||
"admin/podman/volumes": {
|
||||
"title": "Volumes",
|
||||
"order": 50,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/volumes"
|
||||
}
|
||||
},
|
||||
"admin/podman/secrets": {
|
||||
"title": "Secrets",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "podman/secrets"
|
||||
}
|
||||
},
|
||||
"admin/podman/stream/logs/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.podman",
|
||||
"function": "container_logs"
|
||||
},
|
||||
"auth": {
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
"admin/podman/stream/top/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.podman",
|
||||
"function": "container_top"
|
||||
},
|
||||
"auth": {
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
"admin/podman/stream/stats/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.podman",
|
||||
"function": "container_stats"
|
||||
},
|
||||
"auth": {
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
"admin/podman/stream/pull": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.podman",
|
||||
"function": "image_pull"
|
||||
},
|
||||
"auth": {
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
"admin/podman/stream/pod_top/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.podman",
|
||||
"function": "pod_top"
|
||||
},
|
||||
"auth": {
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
},
|
||||
"admin/podman/stream/pod_stats/*": {
|
||||
"action": {
|
||||
"type": "function",
|
||||
"module": "luci.controller.podman",
|
||||
"function": "pod_stats"
|
||||
},
|
||||
"auth": {
|
||||
"methods": [
|
||||
"cookie:sysauth_https",
|
||||
"cookie:sysauth_http"
|
||||
],
|
||||
"login": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
# Podman's restart policy handles runtime restarts to avoid conflicts
|
||||
|
||||
START={start_priority}
|
||||
STOP=20
|
||||
USE_PROCD=1
|
||||
|
||||
NAME={script_name}
|
||||
PROG=/usr/bin/podman
|
||||
|
||||
case "{name}" in
|
||||
*[!A-Za-z0-9._-]*|'')
|
||||
logger -t "${NAME}" "Invalid container name '{name}'; only [A-Za-z0-9._-] allowed"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
start_service() {
|
||||
# Register procd instance - socket wait happens inside command (non-blocking)
|
||||
procd_open_instance "${NAME}"
|
||||
procd_set_param command /bin/sh -c "
|
||||
# Wait for Podman socket with timeout (120s)
|
||||
max_wait=120
|
||||
count=0
|
||||
logger -t ${NAME} 'Waiting for Podman socket...'
|
||||
while [ \"\$count\" -lt \"\$max_wait\" ]; do
|
||||
[ -S /run/podman/podman.sock ] && break
|
||||
sleep 1
|
||||
count=\$((count + 1))
|
||||
done
|
||||
if [ ! -S /run/podman/podman.sock ]; then
|
||||
logger -t ${NAME} 'Timeout: Podman socket not available'
|
||||
exit 1
|
||||
fi
|
||||
logger -t ${NAME} \"Podman socket available after \${count}s\"
|
||||
|
||||
# Check container exists
|
||||
if ! $PROG container exists '{name}' 2>/dev/null; then
|
||||
logger -t ${NAME} 'Container {name} does not exist'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start container if not running
|
||||
if ! $PROG container inspect '{name}' --format '{{.State.Running}}' 2>/dev/null | grep -q true; then
|
||||
logger -t ${NAME} 'Starting container {name}'
|
||||
$PROG start '{name}' || exit 1
|
||||
else
|
||||
logger -t ${NAME} 'Container {name} already running'
|
||||
fi
|
||||
"
|
||||
# Bounded retries: if S99podman's tie-break puts it after this script,
|
||||
# the socket wait above may time out on a slow first boot. A capped
|
||||
# respawn gives extra 120s windows without looping forever.
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
logger -t ${NAME} "Stopping container {name}"
|
||||
$PROG stop "{name}" 2>/dev/null || true
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
import { open, unlink } from 'fs';
|
||||
import { urlencode, ENCODE_FULL } from 'lucihttp'; // ucode-lsp disable
|
||||
import * as podman_socket from 'luci.podman_socket'; // ucode-lsp disable
|
||||
import { API_BASE } from 'luci.podman_socket'; // ucode-lsp disable
|
||||
import { build_request, parse_status, read_headers } from 'luci.podman_http'; // ucode-lsp disable
|
||||
|
||||
const BLOCKSIZE = 4096;
|
||||
|
||||
const reference = ARGV[0];
|
||||
const logfile = ARGV[1];
|
||||
const pidfile = ARGV[2];
|
||||
|
||||
/**
|
||||
* @param {string} msg
|
||||
*/
|
||||
function write_error(msg) {
|
||||
let f = open(logfile, 'a');
|
||||
if (f) {
|
||||
f.write('{"error":"' + replace(replace(msg, /\\/g, '\\\\'), /"/g, '\\"') + '"}\n');
|
||||
f.flush();
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {int} code
|
||||
*/
|
||||
function cleanup(code) {
|
||||
unlink(pidfile);
|
||||
exit(code ?? 0);
|
||||
}
|
||||
|
||||
if (!reference || !logfile || !pidfile)
|
||||
exit(1);
|
||||
|
||||
// Write own PID to pidfile so the streaming endpoint can check if we are running
|
||||
let ps = open('/proc/self/stat', 'r');
|
||||
if (ps) {
|
||||
let pid = +(split(ps.read(200) || '0', ' ')[0]);
|
||||
ps.close();
|
||||
let pf = open(pidfile, 'w');
|
||||
if (pf) { pf.write(pid + '\n'); pf.close(); }
|
||||
}
|
||||
|
||||
// Connect to Podman socket
|
||||
let sock = podman_socket.connect();
|
||||
if (!sock) {
|
||||
write_error('Cannot connect to Podman socket');
|
||||
cleanup(1);
|
||||
}
|
||||
|
||||
let encoded = urlencode(reference, ENCODE_FULL);
|
||||
|
||||
sock.send(build_request(
|
||||
'POST',
|
||||
sprintf('%s/images/pull?reference=%s&quiet=false', API_BASE, encoded),
|
||||
null
|
||||
));
|
||||
|
||||
// Read HTTP response headers (blocking recv - no uloop needed in standalone process)
|
||||
let lf = null;
|
||||
|
||||
let hdrs = read_headers(sock, BLOCKSIZE);
|
||||
if (!hdrs) {
|
||||
write_error('Podman closed connection before responding');
|
||||
sock.close();
|
||||
cleanup(1);
|
||||
}
|
||||
|
||||
let code = parse_status(hdrs.header_buf) || 502;
|
||||
let body = hdrs.body_remainder;
|
||||
|
||||
if (code !== 200) {
|
||||
let parsed = null;
|
||||
try { parsed = json(body); } catch(e) {}
|
||||
let msg = (parsed?.message) || (parsed?.cause)
|
||||
|| replace(body, /\s+$/, '')
|
||||
|| sprintf('Podman error %d', code);
|
||||
write_error(msg);
|
||||
sock.close();
|
||||
cleanup(1);
|
||||
}
|
||||
|
||||
lf = open(logfile, 'a');
|
||||
if (!lf) { sock.close(); cleanup(1); }
|
||||
|
||||
// Write any body bytes that arrived together with headers
|
||||
if (length(body)) { lf.write(body); lf.flush(); }
|
||||
|
||||
// Stream response body to logfile - Podman sends NDJSON, we write it as-is
|
||||
while (true) {
|
||||
let chunk = sock.recv(BLOCKSIZE);
|
||||
if (!chunk) break; // EOF or error - Podman finished
|
||||
lf.write(chunk);
|
||||
lf.flush();
|
||||
}
|
||||
|
||||
lf.close();
|
||||
sock.close();
|
||||
cleanup(0);
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"luci-app-podman": {
|
||||
"description": "Grant access to Podman RPC methods",
|
||||
"read": {
|
||||
"cgi-io": [
|
||||
"exec"
|
||||
],
|
||||
"file": {
|
||||
"/usr/libexec/podman-api volume_export *": [
|
||||
"exec"
|
||||
]
|
||||
},
|
||||
"ubus": {
|
||||
"podman": [
|
||||
"containers_list",
|
||||
"container_inspect",
|
||||
"container_stats",
|
||||
"container_top",
|
||||
"container_healthcheck_run",
|
||||
"images_list",
|
||||
"image_inspect",
|
||||
"image_manifest_inspect",
|
||||
"networks_list",
|
||||
"network_inspect",
|
||||
"volumes_list",
|
||||
"volume_inspect",
|
||||
"pods_list",
|
||||
"pod_inspect",
|
||||
"secrets_list",
|
||||
"secret_inspect",
|
||||
"system_df",
|
||||
"version",
|
||||
"info",
|
||||
"init_script_status",
|
||||
"init_script_show",
|
||||
"system_debug"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"luci-podman",
|
||||
"firewall",
|
||||
"network",
|
||||
"dhcp"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"cgi-io": [
|
||||
"exec",
|
||||
"upload"
|
||||
],
|
||||
"file": {
|
||||
"/tmp/podman-import": [
|
||||
"write"
|
||||
]
|
||||
},
|
||||
"ubus": {
|
||||
"podman": [
|
||||
"container_start",
|
||||
"container_stop",
|
||||
"container_restart",
|
||||
"container_pause",
|
||||
"container_unpause",
|
||||
"container_remove",
|
||||
"container_create",
|
||||
"container_rename",
|
||||
"container_update",
|
||||
"container_healthcheck_run",
|
||||
"image_remove",
|
||||
"image_pull",
|
||||
"network_create",
|
||||
"network_remove",
|
||||
"network_connect",
|
||||
"network_disconnect",
|
||||
"volume_create",
|
||||
"volume_remove",
|
||||
"volume_import",
|
||||
"pod_start",
|
||||
"pod_stop",
|
||||
"pod_kill",
|
||||
"pod_restart",
|
||||
"pod_pause",
|
||||
"pod_unpause",
|
||||
"pod_remove",
|
||||
"pod_create",
|
||||
"secret_create",
|
||||
"secret_remove",
|
||||
"system_prune",
|
||||
"init_script_generate",
|
||||
"init_script_set_enabled",
|
||||
"init_script_remove"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"luci-podman",
|
||||
"firewall",
|
||||
"network",
|
||||
"dhcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,987 @@
|
||||
'use strict';
|
||||
|
||||
// --- Table of Contents ---
|
||||
//
|
||||
// Infrastructure (lines 30-225)
|
||||
// Configuration, validation, HTTP client, helpers
|
||||
//
|
||||
// RPC Methods (line 228+)
|
||||
// Containers: list inspect start stop restart remove stats create rename
|
||||
// update healthcheck top
|
||||
// Images: list inspect remove manifest_inspect pull
|
||||
// Networks: list inspect remove create connect disconnect
|
||||
// Volumes: list inspect remove create import
|
||||
// Pods: list inspect start stop restart pause unpause remove create stats
|
||||
// Secrets: list inspect create remove
|
||||
// System: df prune version info debug
|
||||
// Init Scripts: generate show status set_enabled remove
|
||||
// (exempt from socket check - operate on /etc/init.d/ only)
|
||||
//
|
||||
// Socket wrapper (end of file)
|
||||
|
||||
import { readfile, writefile, popen, stat, chmod, unlink, glob, access } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { urlencode, ENCODE_FULL } from 'lucihttp'; // ucode-lsp disable
|
||||
import { init_enabled, init_action } from 'luci.sys'; // ucode-lsp disable
|
||||
import * as podman_socket from 'luci.podman_socket'; // ucode-lsp disable
|
||||
import { API_BASE } from 'luci.podman_socket'; // ucode-lsp disable
|
||||
import {
|
||||
validate_id, validate_name, validate_image_ref, validate_query_params,
|
||||
validate_restart_policy, require_param, BODY_KEYS
|
||||
} from 'luci.podman_validate'; // ucode-lsp disable
|
||||
import {
|
||||
build_request, parse_status, parse_content_length, read_headers
|
||||
} from 'luci.podman_http'; // ucode-lsp disable
|
||||
|
||||
// --- Configuration ---
|
||||
|
||||
const uci = cursor();
|
||||
const _prio = uci.get('luci-podman', 'globals', 'init_start_priority');
|
||||
// Capped at 99: procd sorts rc.d symlinks as plain strings, so a 3-digit
|
||||
// value like 100 sorts *before* 2-digit priorities (e.g. network at 19-20).
|
||||
const INIT_START_PRIORITY = (type(_prio) === 'string' && match(_prio, /^([0-9]|[1-9][0-9])$/)) ? _prio : '99';
|
||||
uci.unload('luci-podman');
|
||||
|
||||
// "podman" is a strict string prefix of this, so generated init scripts
|
||||
// always sort after the real /etc/init.d/podman at any tied START value.
|
||||
const INIT_SCRIPT_PREFIX = 'podman-container-';
|
||||
|
||||
// Validators come from luci.podman_validate (imported above).
|
||||
|
||||
// --- HTTP Client ---
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {string} path
|
||||
* @param {string} body
|
||||
* @param {bool} raw
|
||||
*/
|
||||
function podman_request(method, path, body, raw) {
|
||||
let sock = podman_socket.connect();
|
||||
if (!sock)
|
||||
return { error: 'Failed to connect to Podman socket' };
|
||||
|
||||
sock.send(build_request(method, path, body));
|
||||
|
||||
let hdrs = read_headers(sock);
|
||||
if (!hdrs) {
|
||||
sock.close();
|
||||
return { error: 'Empty response from Podman API' };
|
||||
}
|
||||
let header_buf = hdrs.header_buf;
|
||||
let body_remainder = hdrs.body_remainder;
|
||||
let chunk = '';
|
||||
|
||||
let status_code = parse_status(header_buf);
|
||||
|
||||
// For 204 No Content (success with no body - e.g., start/stop/restart)
|
||||
if (status_code === 204) {
|
||||
sock.close();
|
||||
if (raw) return { status: 204, body: '' };
|
||||
return {};
|
||||
}
|
||||
|
||||
let content_length = parse_content_length(header_buf);
|
||||
|
||||
// Read body
|
||||
let resp_body = `${body_remainder}`;
|
||||
if (content_length >= 0) {
|
||||
// Read exactly content_length bytes
|
||||
while (length(resp_body) < content_length) {
|
||||
chunk = sock.recv(65536);
|
||||
if (!chunk)
|
||||
break;
|
||||
resp_body += `${chunk}`;
|
||||
}
|
||||
} else {
|
||||
// No Content-Length - read until EOF
|
||||
while (true) {
|
||||
chunk = sock.recv(65536);
|
||||
if (!chunk)
|
||||
break;
|
||||
resp_body += `${chunk}`;
|
||||
}
|
||||
}
|
||||
|
||||
sock.close();
|
||||
|
||||
// Raw mode: return status + body without JSON parsing
|
||||
if (raw) {
|
||||
if (status_code >= 400)
|
||||
return { error: trim(resp_body || `HTTP ${status_code}`) };
|
||||
return { status: status_code, body: resp_body };
|
||||
}
|
||||
|
||||
// Try to parse JSON body
|
||||
if (resp_body !== '') {
|
||||
let parsed = null;
|
||||
try { parsed = json(resp_body); } catch(e) {}
|
||||
|
||||
if (parsed != null) {
|
||||
// Wrap arrays in { data: [...] } for frontend compatibility
|
||||
if (type(parsed) === 'array')
|
||||
return { data: parsed };
|
||||
return parsed;
|
||||
}
|
||||
// Non-JSON response (e.g., plain text error)
|
||||
if (status_code >= 400)
|
||||
return { error: trim(resp_body) };
|
||||
return { data: resp_body };
|
||||
}
|
||||
|
||||
if (status_code >= 400)
|
||||
return { error: `HTTP ${status_code}` };
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function encode_id(id) {
|
||||
return urlencode(id, ENCODE_FULL);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
*/
|
||||
function build_bool_query(params) {
|
||||
let parts = [];
|
||||
for (let k in params) {
|
||||
if (params[k] === true || params[k] === 1)
|
||||
push(parts, `${k}=true`);
|
||||
}
|
||||
return length(parts) ? '?' + join('&', parts) : '';
|
||||
}
|
||||
|
||||
/** @param {any} data */
|
||||
function to_json_body(data) {
|
||||
return (type(data) === 'string') ? data : sprintf('%J', data);
|
||||
}
|
||||
|
||||
// --- Init Script Helpers ---
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
function init_script_path(name) {
|
||||
return `/etc/init.d/${INIT_SCRIPT_PREFIX}${name}`;
|
||||
}
|
||||
|
||||
// --- RPC Methods ---
|
||||
|
||||
const methods = {
|
||||
// ==================== Containers ====================
|
||||
|
||||
containers_list: {
|
||||
args: { query: '' },
|
||||
call: function(req) {
|
||||
let path = `${API_BASE}/containers/json`;
|
||||
if (req.args.query && req.args.query !== '') {
|
||||
let err = validate_query_params(req.args.query);
|
||||
if (err) return { error: err };
|
||||
path += `?${req.args.query}`;
|
||||
}
|
||||
return podman_request('GET', path);
|
||||
}
|
||||
},
|
||||
|
||||
container_inspect: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/containers/${encode_id(req.args.id)}/json`);
|
||||
}
|
||||
},
|
||||
|
||||
container_start: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/start`);
|
||||
}
|
||||
},
|
||||
|
||||
container_stop: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/stop`);
|
||||
}
|
||||
},
|
||||
|
||||
container_restart: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/restart`);
|
||||
}
|
||||
},
|
||||
|
||||
container_pause: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/pause`);
|
||||
}
|
||||
},
|
||||
|
||||
container_unpause: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/unpause`);
|
||||
}
|
||||
},
|
||||
|
||||
container_remove: {
|
||||
args: { id: '', force: false, depend: false },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
let query = build_bool_query({ force: req.args.force, depend: req.args.depend });
|
||||
return podman_request('DELETE', `${API_BASE}/containers/${encode_id(req.args.id)}${query}`);
|
||||
}
|
||||
},
|
||||
|
||||
container_stats: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/containers/${encode_id(req.args.id)}/stats?stream=false`);
|
||||
}
|
||||
},
|
||||
|
||||
container_create: {
|
||||
args: { data: {} },
|
||||
call: function(req) {
|
||||
let data = req.args.data;
|
||||
let err = require_param('data', data);
|
||||
if (err) return { error: err };
|
||||
let body = to_json_body(data);
|
||||
return podman_request('POST', `${API_BASE}/containers/create`, body);
|
||||
}
|
||||
},
|
||||
|
||||
container_rename: {
|
||||
args: { id: '', name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id)
|
||||
|| require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
let name_enc = encode_id(req.args.name);
|
||||
return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/rename?name=${name_enc}`);
|
||||
}
|
||||
},
|
||||
|
||||
container_update: {
|
||||
args: { id: '', data: {} },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id)
|
||||
|| require_param('data', req.args.data);
|
||||
if (err) return { error: err };
|
||||
|
||||
let data = req.args.data;
|
||||
if (type(data) === 'string')
|
||||
data = json(data);
|
||||
if (!data)
|
||||
return { error: 'Invalid JSON data' };
|
||||
|
||||
let id_enc = encode_id(req.args.id);
|
||||
|
||||
// Build query params for restart policy
|
||||
let query_parts = [];
|
||||
if (data.RestartPolicy) {
|
||||
let perr = validate_restart_policy(data.RestartPolicy);
|
||||
if (perr) return { error: perr };
|
||||
push(query_parts, `restartPolicy=${data.RestartPolicy}`);
|
||||
}
|
||||
if (data.RestartRetries != null)
|
||||
push(query_parts, `restartRetries=${+data.RestartRetries | 0}`);
|
||||
|
||||
let query = length(query_parts) ? '?' + join('&', query_parts) : '';
|
||||
|
||||
// Determine if body is needed (resource/health updates)
|
||||
let body_str = sprintf('%J', data);
|
||||
let has_body_fields = false;
|
||||
for (let k in data) {
|
||||
if (k in BODY_KEYS) { has_body_fields = true; break; }
|
||||
}
|
||||
|
||||
return podman_request('POST', `${API_BASE}/containers/${id_enc}/update${query}`,
|
||||
has_body_fields ? body_str : '{}');
|
||||
}
|
||||
},
|
||||
|
||||
container_healthcheck_run: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/containers/${encode_id(req.args.id)}/healthcheck`);
|
||||
}
|
||||
},
|
||||
|
||||
container_top: {
|
||||
args: { id: '', ps_args: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_id(req.args.id);
|
||||
if (err) return { error: err };
|
||||
let path = `${API_BASE}/containers/${encode_id(req.args.id)}/top`;
|
||||
if (req.args.ps_args && req.args.ps_args !== '')
|
||||
path += `?ps_args=${encode_id(req.args.ps_args)}`;
|
||||
return podman_request('GET', path);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Images ====================
|
||||
|
||||
images_list: {
|
||||
args: {},
|
||||
call: function() {
|
||||
return podman_request('GET', `${API_BASE}/images/json`);
|
||||
}
|
||||
},
|
||||
|
||||
image_inspect: {
|
||||
args: { id: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_image_ref(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/images/${encode_id(req.args.id)}/json`);
|
||||
}
|
||||
},
|
||||
|
||||
image_remove: {
|
||||
args: { id: '', force: false },
|
||||
call: function(req) {
|
||||
let err = require_param('id', req.args.id) || validate_image_ref(req.args.id);
|
||||
if (err) return { error: err };
|
||||
return podman_request('DELETE', `${API_BASE}/images/${encode_id(req.args.id)}${build_bool_query({ force: req.args.force })}`);
|
||||
}
|
||||
},
|
||||
|
||||
image_manifest_inspect: {
|
||||
args: { image: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('image', req.args.image) || validate_image_ref(req.args.image);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/manifests/${encode_id(req.args.image)}/json`);
|
||||
}
|
||||
},
|
||||
|
||||
image_pull: {
|
||||
args: { image: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('image', req.args.image) || validate_image_ref(req.args.image);
|
||||
if (err) return { error: err };
|
||||
|
||||
let image_enc = urlencode(req.args.image, ENCODE_FULL);
|
||||
let resp = podman_request('POST', `${API_BASE}/images/pull?reference=${image_enc}`, null, true);
|
||||
if (resp.error) return resp;
|
||||
|
||||
// Response is newline-delimited JSON: {"stream":"..."} lines
|
||||
// Last line contains {"images":[...],"id":"..."}
|
||||
let body = `${resp.body || ''}`;
|
||||
let output = '';
|
||||
let images = null;
|
||||
let id = null;
|
||||
|
||||
let lines = split(body, '\n');
|
||||
for (let i = 0; i < length(lines); i++) {
|
||||
let raw = lines[i];
|
||||
if (type(raw) !== 'string') continue;
|
||||
let line = trim(raw);
|
||||
if (!line) continue;
|
||||
let parsed = null;
|
||||
try { parsed = json(line); } catch(e) {}
|
||||
if (!parsed) continue;
|
||||
|
||||
if (parsed.stream)
|
||||
output += parsed.stream;
|
||||
if (parsed.images)
|
||||
images = parsed.images;
|
||||
if (parsed.id)
|
||||
id = parsed.id;
|
||||
if (parsed.error)
|
||||
return { error: parsed.error };
|
||||
}
|
||||
|
||||
return { output: output, images: images, id: id };
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Networks ====================
|
||||
|
||||
networks_list: {
|
||||
args: {},
|
||||
call: function() {
|
||||
return podman_request('GET', `${API_BASE}/networks/json`);
|
||||
}
|
||||
},
|
||||
|
||||
network_inspect: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/networks/${encode_id(req.args.name)}/json`);
|
||||
}
|
||||
},
|
||||
|
||||
network_remove: {
|
||||
args: { name: '', force: false },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('DELETE', `${API_BASE}/networks/${encode_id(req.args.name)}${build_bool_query({ force: req.args.force })}`);
|
||||
}
|
||||
},
|
||||
|
||||
network_create: {
|
||||
args: { data: {} },
|
||||
call: function(req) {
|
||||
let data = req.args.data;
|
||||
let err = require_param('data', data);
|
||||
if (err) return { error: err };
|
||||
let body = to_json_body(data);
|
||||
return podman_request('POST', `${API_BASE}/networks/create`, body);
|
||||
}
|
||||
},
|
||||
|
||||
network_connect: {
|
||||
args: { name: '', data: {} },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name)
|
||||
|| require_param('data', req.args.data);
|
||||
if (err) return { error: err };
|
||||
let body = to_json_body(req.args.data);
|
||||
return podman_request('POST', `${API_BASE}/networks/${encode_id(req.args.name)}/connect`, body);
|
||||
}
|
||||
},
|
||||
|
||||
network_disconnect: {
|
||||
args: { name: '', data: {} },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name)
|
||||
|| require_param('data', req.args.data);
|
||||
if (err) return { error: err };
|
||||
let body = to_json_body(req.args.data);
|
||||
return podman_request('POST', `${API_BASE}/networks/${encode_id(req.args.name)}/disconnect`, body);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Volumes ====================
|
||||
|
||||
volumes_list: {
|
||||
args: {},
|
||||
call: function() {
|
||||
return podman_request('GET', `${API_BASE}/volumes/json`);
|
||||
}
|
||||
},
|
||||
|
||||
volume_inspect: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/volumes/${encode_id(req.args.name)}/json`);
|
||||
}
|
||||
},
|
||||
|
||||
volume_remove: {
|
||||
args: { name: '', force: false },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('DELETE', `${API_BASE}/volumes/${encode_id(req.args.name)}${build_bool_query({ force: req.args.force })}`);
|
||||
}
|
||||
},
|
||||
|
||||
volume_create: {
|
||||
args: { data: {} },
|
||||
call: function(req) {
|
||||
let data = req.args.data;
|
||||
let err = require_param('data', data);
|
||||
if (err) return { error: err };
|
||||
let body = to_json_body(data);
|
||||
return podman_request('POST', `${API_BASE}/volumes/create`, body);
|
||||
}
|
||||
},
|
||||
|
||||
volume_import: {
|
||||
args: { name: '', compressed: false },
|
||||
call: function(req) {
|
||||
let name = req.args.name;
|
||||
let err = require_param('name', name) || validate_name(name);
|
||||
if (err) return { error: err };
|
||||
|
||||
let filepath = '/tmp/podman-import';
|
||||
if (!stat(filepath))
|
||||
return { error: 'Upload file not found' };
|
||||
|
||||
let existing = podman_request('GET', `${API_BASE}/volumes/${encode_id(name)}/json`);
|
||||
if (!existing.Name) {
|
||||
let created = podman_request('POST', `${API_BASE}/volumes/create`,
|
||||
sprintf('%J', { Name: name }));
|
||||
if (created.error) return created;
|
||||
}
|
||||
|
||||
let cmd;
|
||||
if (req.args.compressed === true || req.args.compressed === 1)
|
||||
cmd = sprintf('gunzip -c "%s" | /usr/bin/podman volume import "%s" - 2>&1', filepath, name);
|
||||
else
|
||||
cmd = sprintf('/usr/bin/podman volume import "%s" "%s" 2>&1', name, filepath);
|
||||
|
||||
let p = popen(cmd, 'r');
|
||||
let output = p ? trim(p.read('all') || '') : '';
|
||||
let exit_code = p ? p.close() : 1;
|
||||
|
||||
unlink(filepath);
|
||||
|
||||
if (exit_code)
|
||||
return { error: output || 'Import failed' };
|
||||
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Pods ====================
|
||||
|
||||
pods_list: {
|
||||
args: {},
|
||||
call: function() {
|
||||
return podman_request('GET', `${API_BASE}/pods/json`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_inspect: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/pods/${encode_id(req.args.name)}/json`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_start: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/start`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_stop: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/stop`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_kill: {
|
||||
args: { name: '', signal: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
let sig = req.args.signal || 'SIGKILL';
|
||||
return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/kill?signal=${sig}`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_restart: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/restart`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_pause: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/pause`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_unpause: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/unpause`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_remove: {
|
||||
args: { name: '', force: false },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('DELETE', `${API_BASE}/pods/${encode_id(req.args.name)}${build_bool_query({ force: req.args.force })}`);
|
||||
}
|
||||
},
|
||||
|
||||
pod_create: {
|
||||
args: { data: {} },
|
||||
call: function(req) {
|
||||
let data = req.args.data;
|
||||
let err = require_param('data', data);
|
||||
if (err) return { error: err };
|
||||
let body = to_json_body(data);
|
||||
return podman_request('POST', `${API_BASE}/pods/create`, body);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Secrets ====================
|
||||
|
||||
secrets_list: {
|
||||
args: {},
|
||||
call: function() {
|
||||
return podman_request('GET', `${API_BASE}/secrets/json`);
|
||||
}
|
||||
},
|
||||
|
||||
secret_inspect: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('GET', `${API_BASE}/secrets/${encode_id(req.args.name)}/json`);
|
||||
}
|
||||
},
|
||||
|
||||
secret_create: {
|
||||
args: { name: '', data: '', labels: {} },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name)
|
||||
|| require_param('data', req.args.data);
|
||||
if (err) return { error: err };
|
||||
let query = `?name=${encode_id(req.args.name)}`;
|
||||
let labels = req.args.labels;
|
||||
if (type(labels) === 'object' && length(labels) > 0)
|
||||
query += `&labels=${urlencode(sprintf('%J', labels), ENCODE_FULL)}`;
|
||||
return podman_request('POST', `${API_BASE}/secrets/create${query}`, `${req.args.data}`);
|
||||
}
|
||||
},
|
||||
|
||||
secret_remove: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
return podman_request('DELETE', `${API_BASE}/secrets/${encode_id(req.args.name)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== System ====================
|
||||
|
||||
system_df: {
|
||||
args: {},
|
||||
call: function() {
|
||||
return podman_request('GET', `${API_BASE}/system/df`);
|
||||
}
|
||||
},
|
||||
|
||||
system_prune: {
|
||||
args: { all: false, volumes: false },
|
||||
call: function(req) {
|
||||
let query = build_bool_query({ all: req.args.all, volumes: req.args.volumes });
|
||||
return podman_request('POST', `${API_BASE}/system/prune${query}`);
|
||||
}
|
||||
},
|
||||
|
||||
version: {
|
||||
args: {},
|
||||
call: function() {
|
||||
return podman_request('GET', `${API_BASE}/version`);
|
||||
}
|
||||
},
|
||||
|
||||
info: {
|
||||
args: {},
|
||||
call: function() {
|
||||
return podman_request('GET', `${API_BASE}/info`);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== System Debug ====================
|
||||
|
||||
system_debug: {
|
||||
args: {},
|
||||
call: function() {
|
||||
let checks = [];
|
||||
|
||||
// 1. Podman binary
|
||||
let podman_stat = stat('/usr/bin/podman');
|
||||
if (podman_stat) {
|
||||
let proc = popen('/usr/bin/podman --version 2>/dev/null', 'r');
|
||||
let ver = proc ? trim(proc.read('line') || '') : '';
|
||||
if (proc) proc.close();
|
||||
push(checks, { name: 'podman_binary', label: 'Podman Binary', status: 'ok', detail: '/usr/bin/podman', message: ver });
|
||||
} else {
|
||||
push(checks, { name: 'podman_binary', label: 'Podman Binary', status: 'error', detail: '/usr/bin/podman', message: 'Not found or not executable' });
|
||||
}
|
||||
|
||||
// 2. Podman socket reachable
|
||||
let dest = podman_socket.get_dest();
|
||||
let local_path = podman_socket.get_local_path();
|
||||
let socket_stat = null;
|
||||
if (!local_path) {
|
||||
push(checks, { name: 'podman_socket', label: 'Podman Socket', status: 'ok', detail: dest, message: 'Remote TCP (cannot stat)' });
|
||||
} else {
|
||||
socket_stat = stat(local_path);
|
||||
if (socket_stat && socket_stat.type === 'socket') {
|
||||
push(checks, { name: 'podman_socket', label: 'Podman Socket', status: 'ok', detail: dest, message: 'Socket file exists' });
|
||||
} else {
|
||||
push(checks, { name: 'podman_socket', label: 'Podman Socket', status: 'error', detail: dest, message: 'Socket not found' });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Socket responsive
|
||||
if (podman_socket.is_remote() || (socket_stat && socket_stat.type === 'socket')) {
|
||||
let ping = podman_request('GET', `${API_BASE}/_ping`);
|
||||
if (ping && ping.data === 'OK') {
|
||||
push(checks, { name: 'socket_responsive', label: 'Socket Responsive', status: 'ok', detail: 'API responding', message: '' });
|
||||
} else {
|
||||
push(checks, { name: 'socket_responsive', label: 'Socket Responsive', status: 'warn', detail: 'API not responding', message: ping ? (ping.error || '') : '' });
|
||||
}
|
||||
} else {
|
||||
push(checks, { name: 'socket_responsive', label: 'Socket Responsive', status: 'error', detail: 'Skipped', message: 'Socket not available' });
|
||||
}
|
||||
|
||||
// 4. ucode-mod-socket
|
||||
push(checks, { name: 'ucode_socket', label: 'ucode-mod-socket', status: 'ok', detail: 'Built-in', message: 'Available (running ucode)' });
|
||||
|
||||
// 5. Init directory writable
|
||||
let init_stat = stat('/etc/init.d');
|
||||
if (init_stat && init_stat.type === 'directory') {
|
||||
// Test writability by checking we can create a temp file
|
||||
let test_path = '/etc/init.d/.podman_write_test';
|
||||
let ok = writefile(test_path, '');
|
||||
if (ok != null) {
|
||||
unlink(test_path);
|
||||
push(checks, { name: 'init_dir_writable', label: 'Init Directory', status: 'ok', detail: '/etc/init.d/', message: 'Writable' });
|
||||
} else {
|
||||
push(checks, { name: 'init_dir_writable', label: 'Init Directory', status: 'warn', detail: '/etc/init.d/', message: 'Not writable - init scripts cannot be created' });
|
||||
}
|
||||
} else {
|
||||
push(checks, { name: 'init_dir_writable', label: 'Init Directory', status: 'error', detail: '/etc/init.d/', message: 'Directory not found' });
|
||||
}
|
||||
|
||||
// 6. Startup template
|
||||
let template = '/usr/share/podman/procd-startup-template.sh';
|
||||
let tmpl_stat = stat(template);
|
||||
if (tmpl_stat) {
|
||||
let content = readfile(template);
|
||||
if (content != null) {
|
||||
push(checks, { name: 'startup_template', label: 'Startup Template', status: 'ok', detail: template, message: 'Readable' });
|
||||
} else {
|
||||
push(checks, { name: 'startup_template', label: 'Startup Template', status: 'warn', detail: template, message: 'Exists but not readable' });
|
||||
}
|
||||
} else {
|
||||
push(checks, { name: 'startup_template', label: 'Startup Template', status: 'warn', detail: template, message: 'Not found - init script generation will fail' });
|
||||
}
|
||||
|
||||
// 7. RPC plugin (self-check - if we're running, we exist)
|
||||
push(checks, { name: 'rpc_plugin', label: 'RPC Plugin', status: 'ok', detail: '/usr/share/rpcd/ucode/podman.uc', message: 'Running (ucode)' });
|
||||
|
||||
// 8. Podman API helper
|
||||
let api_helper = '/usr/libexec/podman-api';
|
||||
let helper_stat = stat(api_helper);
|
||||
if (access(api_helper, 'x')) {
|
||||
push(checks, { name: 'podman_api_helper', label: 'Podman API Helper', status: 'ok', detail: api_helper, message: 'Executable' });
|
||||
} else if (stat(api_helper)) {
|
||||
push(checks, { name: 'podman_api_helper', label: 'Podman API Helper', status: 'warn', detail: api_helper, message: 'Exists but not executable' });
|
||||
} else {
|
||||
push(checks, { name: 'podman_api_helper', label: 'Podman API Helper', status: 'warn', detail: api_helper, message: 'Not found - volume export/import will fail' });
|
||||
}
|
||||
|
||||
// 9. UCI config
|
||||
let uci_ctx = cursor();
|
||||
let socket_cfg = uci_ctx.get('luci-podman', 'globals', 'socket_path');
|
||||
let globals_ok = uci_ctx.get('luci-podman', 'globals');
|
||||
uci_ctx.unload('luci-podman');
|
||||
if (globals_ok) {
|
||||
push(checks, { name: 'uci_config', label: 'UCI Config', status: 'ok', detail: 'luci-podman.globals', message: `socket_path=${socket_cfg || 'default'}` });
|
||||
} else {
|
||||
push(checks, { name: 'uci_config', label: 'UCI Config', status: 'warn', detail: '/etc/config/luci-podman', message: 'Config not found or not loadable' });
|
||||
}
|
||||
|
||||
// 10. Containers config
|
||||
let containers_conf = '/etc/containers/containers.conf';
|
||||
let cc_stat = stat(containers_conf);
|
||||
if (cc_stat) {
|
||||
push(checks, { name: 'containers_conf', label: 'Containers Config', status: 'ok', detail: containers_conf, message: 'Readable' });
|
||||
} else {
|
||||
push(checks, { name: 'containers_conf', label: 'Containers Config', status: 'warn', detail: containers_conf, message: 'Not found - using Podman defaults' });
|
||||
}
|
||||
|
||||
// 11. Network config directory
|
||||
let net_dir = '/etc/containers/networks';
|
||||
let nd_stat = stat(net_dir);
|
||||
if (nd_stat && nd_stat.type === 'directory') {
|
||||
let count = length(glob('/etc/containers/networks/*.json') ?? []);
|
||||
push(checks, { name: 'network_config_dir', label: 'Network Config Dir', status: 'ok', detail: net_dir, message: `${count} network(s)` });
|
||||
} else {
|
||||
push(checks, { name: 'network_config_dir', label: 'Network Config Dir', status: 'warn', detail: net_dir, message: 'Not found' });
|
||||
}
|
||||
|
||||
return { checks: checks };
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== Init Scripts ====================
|
||||
|
||||
init_script_generate: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
|
||||
let name = `${req.args.name}`;
|
||||
let start_priority = `${INIT_START_PRIORITY}`;
|
||||
let script_name = `${INIT_SCRIPT_PREFIX}${name}`;
|
||||
let script_path = init_script_path(name);
|
||||
|
||||
let template = readfile('/usr/share/podman/procd-startup-template.sh');
|
||||
if (!template)
|
||||
return { error: 'Failed to read startup template' };
|
||||
|
||||
let content = replace(template, /\{name\}/g, name);
|
||||
content = replace(content, /\{start_priority\}/g, start_priority);
|
||||
content = replace(content, /\{script_name\}/g, script_name);
|
||||
|
||||
let written = writefile(script_path, content);
|
||||
if (written == null)
|
||||
return { error: 'Failed to create init script' };
|
||||
|
||||
chmod(script_path, 0755);
|
||||
|
||||
return { success: true, path: script_path };
|
||||
}
|
||||
},
|
||||
|
||||
init_script_show: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
|
||||
let path = init_script_path(req.args.name);
|
||||
let content = readfile(path);
|
||||
if (content == null)
|
||||
return { error: 'Init script not found' };
|
||||
|
||||
return { content: content, path: path };
|
||||
}
|
||||
},
|
||||
|
||||
init_script_status: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
|
||||
let script_path = init_script_path(req.args.name);
|
||||
let exists = !!stat(script_path);
|
||||
let enabled = false;
|
||||
|
||||
if (exists)
|
||||
enabled = init_enabled(`${INIT_SCRIPT_PREFIX}${req.args.name}`);
|
||||
|
||||
return { exists: exists, enabled: enabled };
|
||||
}
|
||||
},
|
||||
|
||||
init_script_set_enabled: {
|
||||
args: { name: '', enabled: false },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
|
||||
let script_path = init_script_path(req.args.name);
|
||||
if (!stat(script_path))
|
||||
return { error: 'Init script not found. Generate it first.' };
|
||||
|
||||
let action = (req.args.enabled === true || req.args.enabled === 1) ? 'enable' : 'disable';
|
||||
let rc = init_action(`${INIT_SCRIPT_PREFIX}${req.args.name}`, action);
|
||||
if (rc)
|
||||
return { error: `Failed to ${action} service` };
|
||||
|
||||
return { success: true, enabled: (action === 'enable') };
|
||||
}
|
||||
},
|
||||
|
||||
init_script_remove: {
|
||||
args: { name: '' },
|
||||
call: function(req) {
|
||||
let err = require_param('name', req.args.name) || validate_name(req.args.name);
|
||||
if (err) return { error: err };
|
||||
|
||||
let script_path = init_script_path(req.args.name);
|
||||
if (!stat(script_path))
|
||||
return { success: true, message: 'Init script does not exist' };
|
||||
|
||||
// Disable before removing
|
||||
init_action(`${INIT_SCRIPT_PREFIX}${req.args.name}`, 'disable');
|
||||
unlink(script_path);
|
||||
|
||||
if (stat(script_path))
|
||||
return { error: 'Failed to remove init script' };
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- Socket check wrapper ---
|
||||
// Wrap all methods except system_debug with a socket availability check
|
||||
|
||||
const no_socket_check = {
|
||||
system_debug: true,
|
||||
init_script_generate: true,
|
||||
init_script_show: true,
|
||||
init_script_status: true,
|
||||
init_script_set_enabled: true,
|
||||
init_script_remove: true
|
||||
};
|
||||
|
||||
const _local_socket_path = podman_socket.get_local_path();
|
||||
|
||||
const wrapped_methods = {};
|
||||
for (let name in methods) {
|
||||
let method = methods[name];
|
||||
if (no_socket_check[name]) {
|
||||
wrapped_methods[name] = method;
|
||||
} else {
|
||||
wrapped_methods[name] = {
|
||||
args: method.args,
|
||||
call: function(req) {
|
||||
// Cheap pre-check for unix sockets only; TCP relies on actual connect failures.
|
||||
if (_local_socket_path) {
|
||||
let s = stat(_local_socket_path);
|
||||
if (!s || s.type !== 'socket')
|
||||
return { error: 'Podman socket not found or not accessible' };
|
||||
}
|
||||
return method.call(req);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { 'podman': wrapped_methods };
|
||||
Reference in new issue
Block a user