Merge pull request #353 from community-scripts/fix/vm_detection
Add VM status check and UI improvements
This commit is contained in:
@@ -45,6 +45,7 @@ interface InstalledScript {
|
|||||||
container_status?: 'running' | 'stopped' | 'unknown';
|
container_status?: 'running' | 'stopped' | 'unknown';
|
||||||
web_ui_ip: string | null;
|
web_ui_ip: string | null;
|
||||||
web_ui_port: number | null;
|
web_ui_port: number | null;
|
||||||
|
is_vm?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InstalledScriptsTab() {
|
export function InstalledScriptsTab() {
|
||||||
@@ -1077,23 +1078,35 @@ export function InstalledScriptsTab() {
|
|||||||
<h2 className="text-2xl font-bold text-foreground mb-4">Installed Scripts</h2>
|
<h2 className="text-2xl font-bold text-foreground mb-4">Installed Scripts</h2>
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
|
||||||
<div className="bg-info/10 border border-info/20 p-4 rounded-lg text-center">
|
<div className="bg-info/10 border border-info/20 p-4 rounded-lg text-center">
|
||||||
<div className="text-2xl font-bold text-info">{stats.total}</div>
|
<div className="text-2xl font-bold text-info">{stats.total}</div>
|
||||||
<div className="text-sm text-info/80">Total Installations</div>
|
<div className="text-sm text-info/80">Total Installations</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-success/10 border border-success/20 p-4 rounded-lg text-center">
|
<div className="bg-success/10 border border-success/20 p-4 rounded-lg text-center">
|
||||||
<div className="text-2xl font-bold text-success">
|
<div className="text-2xl font-bold text-success">
|
||||||
{scriptsWithStatus.filter(script => script.container_status === 'running').length}
|
{scriptsWithStatus.filter(script => script.container_status === 'running' && !script.is_vm).length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-success/80">Running LXC</div>
|
<div className="text-sm text-success/80">Running LXC</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-success/10 border border-success/20 p-4 rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-success">
|
||||||
|
{scriptsWithStatus.filter(script => script.container_status === 'running' && script.is_vm).length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-success/80">Running VMs</div>
|
||||||
|
</div>
|
||||||
<div className="bg-error/10 border border-error/20 p-4 rounded-lg text-center">
|
<div className="bg-error/10 border border-error/20 p-4 rounded-lg text-center">
|
||||||
<div className="text-2xl font-bold text-error">
|
<div className="text-2xl font-bold text-error">
|
||||||
{scriptsWithStatus.filter(script => script.container_status === 'stopped').length}
|
{scriptsWithStatus.filter(script => script.container_status === 'stopped' && !script.is_vm).length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-error/80">Stopped LXC</div>
|
<div className="text-sm text-error/80">Stopped LXC</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bg-error/10 border border-error/20 p-4 rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-error">
|
||||||
|
{scriptsWithStatus.filter(script => script.container_status === 'stopped' && script.is_vm).length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-error/80">Stopped VMs</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1527,7 +1540,18 @@ export function InstalledScriptsTab() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-foreground">{script.script_name}</div>
|
<div className="flex items-center gap-2">
|
||||||
|
{script.container_id && (
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded font-medium ${
|
||||||
|
script.is_vm
|
||||||
|
? 'bg-purple-500/20 text-purple-600 dark:text-purple-400 border border-purple-500/30'
|
||||||
|
: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 border border-blue-500/30'
|
||||||
|
}`}>
|
||||||
|
{script.is_vm ? 'VM' : 'LXC'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="text-sm font-medium text-foreground">{script.script_name}</div>
|
||||||
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">{script.script_path}</div>
|
<div className="text-sm text-muted-foreground">{script.script_path}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1683,7 +1707,7 @@ export function InstalledScriptsTab() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-48 bg-card border-border">
|
<DropdownMenuContent className="w-48 bg-card border-border">
|
||||||
{script.container_id && (
|
{script.container_id && !script.is_vm && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleUpdateScript(script)}
|
onClick={() => handleUpdateScript(script)}
|
||||||
disabled={containerStatuses.get(script.id) === 'stopped'}
|
disabled={containerStatuses.get(script.id) === 'stopped'}
|
||||||
@@ -1701,7 +1725,7 @@ export function InstalledScriptsTab() {
|
|||||||
Backup
|
Backup
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{script.container_id && script.execution_mode === 'ssh' && (
|
{script.container_id && script.execution_mode === 'ssh' && !script.is_vm && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleOpenShell(script)}
|
onClick={() => handleOpenShell(script)}
|
||||||
disabled={containerStatuses.get(script.id) === 'stopped'}
|
disabled={containerStatuses.get(script.id) === 'stopped'}
|
||||||
@@ -1728,7 +1752,7 @@ export function InstalledScriptsTab() {
|
|||||||
{autoDetectWebUIMutation.isPending ? 'Re-detect...' : 'Re-detect IP/Port'}
|
{autoDetectWebUIMutation.isPending ? 'Re-detect...' : 'Re-detect IP/Port'}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{script.container_id && script.execution_mode === 'ssh' && (
|
{script.container_id && script.execution_mode === 'ssh' && !script.is_vm && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator className="bg-border" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -1739,6 +1763,11 @@ export function InstalledScriptsTab() {
|
|||||||
LXC Settings
|
LXC Settings
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="bg-border" />
|
<DropdownMenuSeparator className="bg-border" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{script.container_id && script.execution_mode === 'ssh' && (
|
||||||
|
<>
|
||||||
|
{script.is_vm && <DropdownMenuSeparator className="bg-border" />}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => handleStartStop(script, (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'stop' : 'start')}
|
onClick={() => handleStartStop(script, (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'stop' : 'start')}
|
||||||
disabled={controllingScriptId === script.id || (containerStatuses.get(script.id) ?? 'unknown') === 'unknown'}
|
disabled={controllingScriptId === script.id || (containerStatuses.get(script.id) ?? 'unknown') === 'unknown'}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface InstalledScript {
|
|||||||
container_status?: 'running' | 'stopped' | 'unknown';
|
container_status?: 'running' | 'stopped' | 'unknown';
|
||||||
web_ui_ip: string | null;
|
web_ui_ip: string | null;
|
||||||
web_ui_port: number | null;
|
web_ui_port: number | null;
|
||||||
|
is_vm?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScriptInstallationCardProps {
|
interface ScriptInstallationCardProps {
|
||||||
@@ -300,7 +301,7 @@ export function ScriptInstallationCard({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-48 bg-card border-border">
|
<DropdownMenuContent className="w-48 bg-card border-border">
|
||||||
{script.container_id && (
|
{script.container_id && !script.is_vm && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={onUpdate}
|
onClick={onUpdate}
|
||||||
disabled={containerStatus === 'stopped'}
|
disabled={containerStatus === 'stopped'}
|
||||||
@@ -318,7 +319,7 @@ export function ScriptInstallationCard({
|
|||||||
Backup
|
Backup
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{script.container_id && script.execution_mode === 'ssh' && (
|
{script.container_id && script.execution_mode === 'ssh' && !script.is_vm && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={onShell}
|
onClick={onShell}
|
||||||
disabled={containerStatus === 'stopped'}
|
disabled={containerStatus === 'stopped'}
|
||||||
|
|||||||
@@ -383,6 +383,88 @@ async function tryLVMResize(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to determine if a container is a VM or LXC
|
||||||
|
async function isVM(scriptId: number, containerId: string, serverId: number | null): Promise<boolean> {
|
||||||
|
const db = getDatabase();
|
||||||
|
|
||||||
|
// Method 1: Check if LXCConfig exists (if exists, it's an LXC container)
|
||||||
|
const lxcConfig = await db.getLXCConfigByScriptId(scriptId);
|
||||||
|
if (lxcConfig) {
|
||||||
|
return false; // Has LXCConfig, so it's an LXC container
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: If no LXCConfig, check config file paths on server
|
||||||
|
if (!serverId) {
|
||||||
|
// Can't determine without server, default to false (LXC) for safety
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const server = await db.getServerById(serverId);
|
||||||
|
if (!server) {
|
||||||
|
return false; // Default to LXC if server not found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import SSH services
|
||||||
|
const { default: SSHService } = await import('~/server/ssh-service');
|
||||||
|
const { default: SSHExecutionService } = await import('~/server/ssh-execution-service');
|
||||||
|
const sshService = new SSHService();
|
||||||
|
const sshExecutionService = new SSHExecutionService();
|
||||||
|
|
||||||
|
// Test SSH connection
|
||||||
|
const connectionTest = await sshService.testSSHConnection(server as Server);
|
||||||
|
if (!(connectionTest as any).success) {
|
||||||
|
return false; // Default to LXC if SSH fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check both config file paths
|
||||||
|
const vmConfigPath = `/etc/pve/qemu-server/${containerId}.conf`;
|
||||||
|
const lxcConfigPath = `/etc/pve/lxc/${containerId}.conf`;
|
||||||
|
|
||||||
|
// Check VM config file
|
||||||
|
let vmConfigExists = false;
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
|
server as Server,
|
||||||
|
`test -f "${vmConfigPath}" && echo "exists" || echo "not_exists"`,
|
||||||
|
(data: string) => {
|
||||||
|
if (data.includes('exists')) {
|
||||||
|
vmConfigExists = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => resolve(),
|
||||||
|
() => resolve()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vmConfigExists) {
|
||||||
|
return true; // VM config file exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check LXC config file
|
||||||
|
let lxcConfigExists = false;
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
|
server as Server,
|
||||||
|
`test -f "${lxcConfigPath}" && echo "exists" || echo "not_exists"`,
|
||||||
|
(data: string) => {
|
||||||
|
if (data.includes('exists')) {
|
||||||
|
lxcConfigExists = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => resolve(),
|
||||||
|
() => resolve()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If LXC config exists, it's an LXC container
|
||||||
|
return !lxcConfigExists; // Return true if it's a VM (neither config exists defaults to false/LXC)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error determining container type:', error);
|
||||||
|
return false; // Default to LXC on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const installedScriptsRouter = createTRPCRouter({
|
export const installedScriptsRouter = createTRPCRouter({
|
||||||
// Get all installed scripts
|
// Get all installed scripts
|
||||||
@@ -393,18 +475,27 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
const scripts = await db.getAllInstalledScripts();
|
const scripts = await db.getAllInstalledScripts();
|
||||||
|
|
||||||
// Transform scripts to flatten server data for frontend compatibility
|
// Transform scripts to flatten server data for frontend compatibility
|
||||||
const transformedScripts = scripts.map(script => ({
|
const transformedScripts = await Promise.all(scripts.map(async (script) => {
|
||||||
...script,
|
// Determine if it's a VM or LXC
|
||||||
server_name: script.server?.name ?? null,
|
let is_vm = false;
|
||||||
server_ip: script.server?.ip ?? null,
|
if (script.container_id && script.server_id) {
|
||||||
server_user: script.server?.user ?? null,
|
is_vm = await isVM(script.id, script.container_id, script.server_id);
|
||||||
server_password: script.server?.password ?? null,
|
}
|
||||||
server_auth_type: script.server?.auth_type ?? null,
|
|
||||||
server_ssh_key: script.server?.ssh_key ?? null,
|
return {
|
||||||
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
...script,
|
||||||
server_ssh_port: script.server?.ssh_port ?? null,
|
server_name: script.server?.name ?? null,
|
||||||
server_color: script.server?.color ?? null,
|
server_ip: script.server?.ip ?? null,
|
||||||
server: undefined // Remove nested server object
|
server_user: script.server?.user ?? null,
|
||||||
|
server_password: script.server?.password ?? null,
|
||||||
|
server_auth_type: script.server?.auth_type ?? null,
|
||||||
|
server_ssh_key: script.server?.ssh_key ?? null,
|
||||||
|
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
||||||
|
server_ssh_port: script.server?.ssh_port ?? null,
|
||||||
|
server_color: script.server?.color ?? null,
|
||||||
|
is_vm,
|
||||||
|
server: undefined // Remove nested server object
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -430,18 +521,27 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
const scripts = await db.getInstalledScriptsByServer(input.serverId);
|
const scripts = await db.getInstalledScriptsByServer(input.serverId);
|
||||||
|
|
||||||
// Transform scripts to flatten server data for frontend compatibility
|
// Transform scripts to flatten server data for frontend compatibility
|
||||||
const transformedScripts = scripts.map(script => ({
|
const transformedScripts = await Promise.all(scripts.map(async (script) => {
|
||||||
...script,
|
// Determine if it's a VM or LXC
|
||||||
server_name: script.server?.name ?? null,
|
let is_vm = false;
|
||||||
server_ip: script.server?.ip ?? null,
|
if (script.container_id && script.server_id) {
|
||||||
server_user: script.server?.user ?? null,
|
is_vm = await isVM(script.id, script.container_id, script.server_id);
|
||||||
server_password: script.server?.password ?? null,
|
}
|
||||||
server_auth_type: script.server?.auth_type ?? null,
|
|
||||||
server_ssh_key: script.server?.ssh_key ?? null,
|
return {
|
||||||
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
...script,
|
||||||
server_ssh_port: script.server?.ssh_port ?? null,
|
server_name: script.server?.name ?? null,
|
||||||
server_color: script.server?.color ?? null,
|
server_ip: script.server?.ip ?? null,
|
||||||
server: undefined // Remove nested server object
|
server_user: script.server?.user ?? null,
|
||||||
|
server_password: script.server?.password ?? null,
|
||||||
|
server_auth_type: script.server?.auth_type ?? null,
|
||||||
|
server_ssh_key: script.server?.ssh_key ?? null,
|
||||||
|
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
||||||
|
server_ssh_port: script.server?.ssh_port ?? null,
|
||||||
|
server_color: script.server?.color ?? null,
|
||||||
|
is_vm,
|
||||||
|
server: undefined // Remove nested server object
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -472,6 +572,12 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
script: null
|
script: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Determine if it's a VM or LXC
|
||||||
|
let is_vm = false;
|
||||||
|
if (script.container_id && script.server_id) {
|
||||||
|
is_vm = await isVM(script.id, script.container_id, script.server_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Transform script to flatten server data for frontend compatibility
|
// Transform script to flatten server data for frontend compatibility
|
||||||
const transformedScript = {
|
const transformedScript = {
|
||||||
...script,
|
...script,
|
||||||
@@ -484,6 +590,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
server_ssh_key_passphrase: script.server?.ssh_key_passphrase ?? null,
|
||||||
server_ssh_port: script.server?.ssh_port ?? null,
|
server_ssh_port: script.server?.ssh_port ?? null,
|
||||||
server_color: script.server?.color ?? null,
|
server_color: script.server?.color ?? null,
|
||||||
|
is_vm,
|
||||||
server: undefined // Remove nested server object
|
server: undefined // Remove nested server object
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -677,113 +784,159 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Use the working approach - manual loop through all config files
|
// Get containers from pct list and VMs from qm list
|
||||||
const command = `for file in /etc/pve/lxc/*.conf; do if [ -f "$file" ]; then if grep -q "community-script" "$file"; then echo "$file"; fi; fi; done`;
|
|
||||||
let detectedContainers: any[] = [];
|
let detectedContainers: any[] = [];
|
||||||
|
|
||||||
|
// Helper function to parse list output and extract IDs
|
||||||
|
const parseListOutput = (output: string, isVM: boolean): string[] => {
|
||||||
|
const ids: string[] = [];
|
||||||
|
const lines = output.split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Skip header lines
|
||||||
|
if (line.includes('VMID') || line.includes('CTID')) continue;
|
||||||
|
|
||||||
|
// Extract first column (ID)
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
if (parts.length > 0) {
|
||||||
|
const id = parts[0]?.trim();
|
||||||
|
// Validate ID format (3-4 digits typically)
|
||||||
|
if (id && /^\d{3,4}$/.test(id)) {
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
};
|
||||||
|
|
||||||
let commandOutput = '';
|
// Helper function to check config file for community-script tag and extract hostname/name
|
||||||
|
const checkConfigAndExtractInfo = async (id: string, isVM: boolean): Promise<any> => {
|
||||||
|
const configPath = isVM
|
||||||
|
? `/etc/pve/qemu-server/${id}.conf`
|
||||||
|
: `/etc/pve/lxc/${id}.conf`;
|
||||||
|
|
||||||
|
const readCommand = `cat "${configPath}" 2>/dev/null`;
|
||||||
|
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
let configData = '';
|
||||||
|
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
|
server as Server,
|
||||||
|
readCommand,
|
||||||
|
(data: string) => {
|
||||||
|
configData += data;
|
||||||
|
},
|
||||||
|
(_error: string) => {
|
||||||
|
// Config file doesn't exist or can't be read
|
||||||
|
resolve(null);
|
||||||
|
},
|
||||||
|
(_exitCode: number) => {
|
||||||
|
// Check if config contains community-script tag
|
||||||
|
if (!configData.includes('community-script')) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract hostname (for containers) or name (for VMs)
|
||||||
|
const lines = configData.split('\n');
|
||||||
|
let hostname = '';
|
||||||
|
let name = '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (trimmedLine.startsWith('hostname:')) {
|
||||||
|
hostname = trimmedLine.substring(9).trim();
|
||||||
|
} else if (trimmedLine.startsWith('name:')) {
|
||||||
|
name = trimmedLine.substring(5).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use hostname for containers, name for VMs
|
||||||
|
const displayName = isVM ? name : hostname;
|
||||||
|
|
||||||
|
if (displayName) {
|
||||||
|
// Parse full config and store in database (only for containers)
|
||||||
|
let parsedConfig = null;
|
||||||
|
let configHash = null;
|
||||||
|
|
||||||
|
if (!isVM) {
|
||||||
|
parsedConfig = parseRawConfig(configData);
|
||||||
|
configHash = calculateConfigHash(configData);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
containerId: id,
|
||||||
|
hostname: displayName,
|
||||||
|
configPath,
|
||||||
|
isVM,
|
||||||
|
serverId: Number((server as any).id),
|
||||||
|
serverName: (server as any).name,
|
||||||
|
parsedConfig: parsedConfig ? {
|
||||||
|
...parsedConfig,
|
||||||
|
config_hash: configHash,
|
||||||
|
synced_at: new Date()
|
||||||
|
} : null
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get containers from pct list
|
||||||
|
let pctOutput = '';
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
|
||||||
void sshExecutionService.executeCommand(
|
void sshExecutionService.executeCommand(
|
||||||
|
|
||||||
server as Server,
|
server as Server,
|
||||||
command,
|
'pct list',
|
||||||
(data: string) => {
|
(data: string) => {
|
||||||
commandOutput += data;
|
pctOutput += data;
|
||||||
},
|
},
|
||||||
(error: string) => {
|
(error: string) => {
|
||||||
console.error('Command error:', error);
|
console.error('pct list error:', error);
|
||||||
|
reject(new Error(`pct list failed: ${error}`));
|
||||||
},
|
},
|
||||||
(_exitCode: number) => {
|
(_exitCode: number) => {
|
||||||
|
resolve();
|
||||||
// Parse the complete output to get config file paths that contain community-script tag
|
|
||||||
const configFiles = commandOutput.split('\n')
|
|
||||||
.filter((line: string) => line.trim())
|
|
||||||
.map((line: string) => line.trim())
|
|
||||||
.filter((line: string) => line.endsWith('.conf'));
|
|
||||||
|
|
||||||
|
|
||||||
// Process each config file to extract hostname
|
|
||||||
const processPromises = configFiles.map(async (configPath: string) => {
|
|
||||||
try {
|
|
||||||
const containerId = configPath.split('/').pop()?.replace('.conf', '');
|
|
||||||
if (!containerId) return null;
|
|
||||||
|
|
||||||
|
|
||||||
// Read the config file content
|
|
||||||
const readCommand = `cat "${configPath}" 2>/dev/null`;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
||||||
return new Promise<any>((readResolve) => {
|
|
||||||
|
|
||||||
void sshExecutionService.executeCommand(
|
|
||||||
|
|
||||||
server as Server,
|
|
||||||
readCommand,
|
|
||||||
(configData: string) => {
|
|
||||||
// Parse config file for hostname
|
|
||||||
const lines = configData.split('\n');
|
|
||||||
let hostname = '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
if (trimmedLine.startsWith('hostname:')) {
|
|
||||||
hostname = trimmedLine.substring(9).trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostname) {
|
|
||||||
// Parse full config and store in database
|
|
||||||
const parsedConfig = parseRawConfig(configData);
|
|
||||||
const configHash = calculateConfigHash(configData);
|
|
||||||
|
|
||||||
const container = {
|
|
||||||
containerId,
|
|
||||||
hostname,
|
|
||||||
configPath,
|
|
||||||
serverId: Number((server as any).id),
|
|
||||||
serverName: (server as any).name,
|
|
||||||
parsedConfig: {
|
|
||||||
...parsedConfig,
|
|
||||||
config_hash: configHash,
|
|
||||||
synced_at: new Date()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
readResolve(container);
|
|
||||||
} else {
|
|
||||||
readResolve(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(readError: string) => {
|
|
||||||
console.error(`Error reading config file ${configPath}:`, readError);
|
|
||||||
readResolve(null);
|
|
||||||
},
|
|
||||||
(_exitCode: number) => {
|
|
||||||
readResolve(null);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error processing config file ${configPath}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for all config files to be processed
|
|
||||||
void Promise.all(processPromises).then((results) => {
|
|
||||||
detectedContainers = results.filter(result => result !== null);
|
|
||||||
resolve();
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Error processing config files:', error);
|
|
||||||
reject(new Error(`Error processing config files: ${error}`));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get VMs from qm list
|
||||||
|
let qmOutput = '';
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
|
server as Server,
|
||||||
|
'qm list',
|
||||||
|
(data: string) => {
|
||||||
|
qmOutput += data;
|
||||||
|
},
|
||||||
|
(error: string) => {
|
||||||
|
console.error('qm list error:', error);
|
||||||
|
reject(new Error(`qm list failed: ${error}`));
|
||||||
|
},
|
||||||
|
(_exitCode: number) => {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse IDs from both lists
|
||||||
|
const containerIds = parseListOutput(pctOutput, false);
|
||||||
|
const vmIds = parseListOutput(qmOutput, true);
|
||||||
|
|
||||||
|
// Check each container/VM for community-script tag
|
||||||
|
const checkPromises = [
|
||||||
|
...containerIds.map(id => checkConfigAndExtractInfo(id, false)),
|
||||||
|
...vmIds.map(id => checkConfigAndExtractInfo(id, true))
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(checkPromises);
|
||||||
|
detectedContainers = results.filter(result => result !== null);
|
||||||
|
|
||||||
|
|
||||||
// Get existing scripts to check for duplicates
|
// Get existing scripts to check for duplicates
|
||||||
const existingScripts = await db.getAllInstalledScripts();
|
const existingScripts = await db.getAllInstalledScripts();
|
||||||
@@ -816,11 +969,11 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
server_id: container.serverId,
|
server_id: container.serverId,
|
||||||
execution_mode: 'ssh',
|
execution_mode: 'ssh',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
output_log: `Auto-detected from LXC config: ${container.configPath}`
|
output_log: `Auto-detected from ${container.isVM ? 'VM' : 'LXC'} config: ${container.configPath}`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store LXC config in database
|
// Store LXC config in database (only for containers, not VMs)
|
||||||
if (container.parsedConfig) {
|
if (container.parsedConfig && !container.isVM) {
|
||||||
await db.createLXCConfig(result.id, container.parsedConfig);
|
await db.createLXCConfig(result.id, container.parsedConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,8 +989,8 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = skippedScripts.length > 0
|
const message = skippedScripts.length > 0
|
||||||
? `Auto-detection completed. Found ${detectedContainers.length} containers with community-script tag. Added ${createdScripts.length} new scripts, skipped ${skippedScripts.length} duplicates.`
|
? `Auto-detection completed. Found ${detectedContainers.length} containers/VMs with community-script tag. Added ${createdScripts.length} new scripts, skipped ${skippedScripts.length} duplicates.`
|
||||||
: `Auto-detection completed. Found ${detectedContainers.length} containers with community-script tag. Added ${createdScripts.length} new scripts.`;
|
: `Auto-detection completed. Found ${detectedContainers.length} containers/VMs with community-script tag. Added ${createdScripts.length} new scripts.`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -920,11 +1073,32 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all existing containers from pct list (more reliable than checking config files)
|
// Helper function to parse list output and extract IDs
|
||||||
const listCommand = 'pct list';
|
const parseListOutput = (output: string): Set<string> => {
|
||||||
let listOutput = '';
|
const ids = new Set<string>();
|
||||||
|
const lines = output.split('\n').filter(line => line.trim());
|
||||||
const existingContainerIds = await new Promise<Set<string>>((resolve, reject) => {
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Skip header lines
|
||||||
|
if (line.includes('VMID') || line.includes('CTID')) continue;
|
||||||
|
|
||||||
|
// Extract first column (ID)
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
if (parts.length > 0) {
|
||||||
|
const id = parts[0]?.trim();
|
||||||
|
// Validate ID format (3-4 digits typically)
|
||||||
|
if (id && /^\d{3,4}$/.test(id)) {
|
||||||
|
ids.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all existing containers from pct list
|
||||||
|
let pctOutput = '';
|
||||||
|
const existingContainerIds = await new Promise<Set<string>>((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
console.warn(`cleanupOrphanedScripts: timeout while getting container list from server ${String((server as any).name)}`);
|
console.warn(`cleanupOrphanedScripts: timeout while getting container list from server ${String((server as any).name)}`);
|
||||||
resolve(new Set()); // Treat timeout as no containers found
|
resolve(new Set()); // Treat timeout as no containers found
|
||||||
@@ -932,9 +1106,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
void sshExecutionService.executeCommand(
|
void sshExecutionService.executeCommand(
|
||||||
server as Server,
|
server as Server,
|
||||||
listCommand,
|
'pct list',
|
||||||
(data: string) => {
|
(data: string) => {
|
||||||
listOutput += data;
|
pctOutput += data;
|
||||||
},
|
},
|
||||||
(error: string) => {
|
(error: string) => {
|
||||||
console.error(`cleanupOrphanedScripts: error getting container list from server ${String((server as any).name)}:`, error);
|
console.error(`cleanupOrphanedScripts: error getting container list from server ${String((server as any).name)}:`, error);
|
||||||
@@ -943,58 +1117,95 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
(_exitCode: number) => {
|
(_exitCode: number) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
resolve(parseListOutput(pctOutput));
|
||||||
// Parse pct list output to extract container IDs
|
|
||||||
const containerIds = new Set<string>();
|
|
||||||
const lines = listOutput.split('\n').filter(line => line.trim());
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
// pct list format: CTID Status Name
|
|
||||||
// Skip header line if present
|
|
||||||
if (line.includes('CTID') || line.includes('VMID')) continue;
|
|
||||||
|
|
||||||
const parts = line.trim().split(/\s+/);
|
|
||||||
if (parts.length > 0) {
|
|
||||||
const containerId = parts[0]?.trim();
|
|
||||||
if (containerId && /^\d{3,4}$/.test(containerId)) {
|
|
||||||
containerIds.add(containerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(containerIds);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check each script against the list of existing containers
|
// Get all existing VMs from qm list
|
||||||
|
let qmOutput = '';
|
||||||
|
const existingVMIds = await new Promise<Set<string>>((resolve) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.warn(`cleanupOrphanedScripts: timeout while getting VM list from server ${String((server as any).name)}`);
|
||||||
|
resolve(new Set()); // Treat timeout as no VMs found
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
|
server as Server,
|
||||||
|
'qm list',
|
||||||
|
(data: string) => {
|
||||||
|
qmOutput += data;
|
||||||
|
},
|
||||||
|
(error: string) => {
|
||||||
|
console.error(`cleanupOrphanedScripts: error getting VM list from server ${String((server as any).name)}:`, error);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(new Set()); // Treat error as no VMs found
|
||||||
|
},
|
||||||
|
(_exitCode: number) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(parseListOutput(qmOutput));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combine both sets - an ID exists if it's in either list
|
||||||
|
const existingIds = new Set<string>([...existingContainerIds, ...existingVMIds]);
|
||||||
|
|
||||||
|
// Check each script against the list of existing containers and VMs
|
||||||
for (const scriptData of serverScripts) {
|
for (const scriptData of serverScripts) {
|
||||||
try {
|
try {
|
||||||
const containerId = String(scriptData.container_id).trim();
|
const containerId = String(scriptData.container_id).trim();
|
||||||
|
|
||||||
// Check if container exists in pct list
|
// Check if ID exists in either pct list (containers) or qm list (VMs)
|
||||||
if (!existingContainerIds.has(containerId)) {
|
if (!existingIds.has(containerId)) {
|
||||||
// Also verify config file doesn't exist as a double-check
|
// Also verify config file doesn't exist as a double-check
|
||||||
const checkCommand = `test -f "/etc/pve/lxc/${containerId}.conf" && echo "exists" || echo "not_found"`;
|
// Check both container and VM config paths
|
||||||
|
const checkContainerCommand = `test -f "/etc/pve/lxc/${containerId}.conf" && echo "exists" || echo "not_found"`;
|
||||||
|
const checkVMCommand = `test -f "/etc/pve/qemu-server/${containerId}.conf" && echo "exists" || echo "not_found"`;
|
||||||
|
|
||||||
const configExists = await new Promise<boolean>((resolve) => {
|
const configExists = await new Promise<boolean>((resolve) => {
|
||||||
let combinedOutput = '';
|
let combinedOutput = '';
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
|
let checksCompleted = 0;
|
||||||
|
|
||||||
const finish = () => {
|
const finish = () => {
|
||||||
if (resolved) return;
|
if (resolved) return;
|
||||||
resolved = true;
|
checksCompleted++;
|
||||||
const out = combinedOutput.trim();
|
if (checksCompleted === 2) {
|
||||||
resolve(out.includes('exists'));
|
resolved = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
const out = combinedOutput.trim();
|
||||||
|
resolve(out.includes('exists'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
finish();
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
const out = combinedOutput.trim();
|
||||||
|
resolve(out.includes('exists'));
|
||||||
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
// Check container config
|
||||||
void sshExecutionService.executeCommand(
|
void sshExecutionService.executeCommand(
|
||||||
server as Server,
|
server as Server,
|
||||||
checkCommand,
|
checkContainerCommand,
|
||||||
|
(data: string) => {
|
||||||
|
combinedOutput += data;
|
||||||
|
},
|
||||||
|
(_error: string) => {
|
||||||
|
// Ignore errors, just check output
|
||||||
|
},
|
||||||
|
(_exitCode: number) => {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check VM config
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
|
server as Server,
|
||||||
|
checkVMCommand,
|
||||||
(data: string) => {
|
(data: string) => {
|
||||||
combinedOutput += data;
|
combinedOutput += data;
|
||||||
},
|
},
|
||||||
@@ -1002,20 +1213,19 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
// Ignore errors, just check output
|
// Ignore errors, just check output
|
||||||
},
|
},
|
||||||
(_exitCode: number) => {
|
(_exitCode: number) => {
|
||||||
clearTimeout(timer);
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// If container is not in pct list AND config file doesn't exist, it's orphaned
|
// If ID is not in either list AND config file doesn't exist, it's orphaned
|
||||||
if (!configExists) {
|
if (!configExists) {
|
||||||
console.log(`cleanupOrphanedScripts: Removing orphaned script ${String(scriptData.script_name)} (container ${containerId}) from server ${String((server as any).name)}`);
|
console.log(`cleanupOrphanedScripts: Removing orphaned script ${String(scriptData.script_name)} (ID ${containerId}) from server ${String((server as any).name)}`);
|
||||||
await db.deleteInstalledScript(Number(scriptData.id));
|
await db.deleteInstalledScript(Number(scriptData.id));
|
||||||
deletedScripts.push(String(scriptData.script_name));
|
deletedScripts.push(String(scriptData.script_name));
|
||||||
} else {
|
} else {
|
||||||
// Config exists but not in pct list - might be in a transitional state, log but don't delete
|
// Config exists but not in lists - might be in a transitional state, log but don't delete
|
||||||
console.warn(`cleanupOrphanedScripts: Container ${containerId} (${String(scriptData.script_name)}) config exists but not in pct list - may be in transitional state`);
|
console.warn(`cleanupOrphanedScripts: Container/VM ${containerId} (${String(scriptData.script_name)}) config exists but not in pct/qm list - may be in transitional state`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1080,59 +1290,120 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run pct list to get all container statuses at once
|
// Helper function to parse list output and extract statuses
|
||||||
const listCommand = 'pct list';
|
const parseListStatuses = (output: string): Record<string, 'running' | 'stopped' | 'unknown'> => {
|
||||||
let listOutput = '';
|
const statuses: Record<string, 'running' | 'stopped' | 'unknown'> = {};
|
||||||
|
const lines = output.split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
|
// Find header line to determine column positions
|
||||||
|
let statusColumnIndex = 1; // Default to second column
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('STATUS')) {
|
||||||
|
// Parse header to find STATUS column index
|
||||||
|
const headerParts = line.trim().split(/\s+/);
|
||||||
|
const statusIndex = headerParts.findIndex(part => part.includes('STATUS'));
|
||||||
|
if (statusIndex >= 0) {
|
||||||
|
statusColumnIndex = statusIndex;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Skip header lines
|
||||||
|
if (line.includes('VMID') || line.includes('CTID') || line.includes('STATUS')) continue;
|
||||||
|
|
||||||
|
// Parse line
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
if (parts.length > statusColumnIndex) {
|
||||||
|
const id = parts[0]?.trim();
|
||||||
|
const status = parts[statusColumnIndex]?.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (id && /^\d+$/.test(id)) { // Validate ID is numeric
|
||||||
|
// Map status to our status format
|
||||||
|
let mappedStatus: 'running' | 'stopped' | 'unknown' = 'unknown';
|
||||||
|
if (status === 'running') {
|
||||||
|
mappedStatus = 'running';
|
||||||
|
} else if (status === 'stopped') {
|
||||||
|
mappedStatus = 'stopped';
|
||||||
|
}
|
||||||
|
// All other statuses (paused, locked, suspended, etc.) map to 'unknown'
|
||||||
|
|
||||||
|
statuses[id] = mappedStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run pct list to get all container statuses
|
||||||
|
let pctOutput = '';
|
||||||
|
|
||||||
// Add timeout to prevent hanging connections
|
// Add timeout to prevent hanging connections
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
setTimeout(() => reject(new Error('SSH command timeout after 30 seconds')), 30000);
|
setTimeout(() => reject(new Error('SSH command timeout after 30 seconds')), 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.race([
|
try {
|
||||||
new Promise<void>((resolve, reject) => {
|
await Promise.race([
|
||||||
void sshExecutionService.executeCommand(
|
new Promise<void>((resolve, reject) => {
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
server as Server,
|
server as Server,
|
||||||
listCommand,
|
'pct list',
|
||||||
(data: string) => {
|
(data: string) => {
|
||||||
listOutput += data;
|
pctOutput += data;
|
||||||
},
|
},
|
||||||
(error: string) => {
|
(error: string) => {
|
||||||
console.error(`pct list error on server ${(server as any).name}:`, error);
|
console.error(`pct list error on server ${(server as any).name}:`, error);
|
||||||
reject(new Error(error));
|
// Don't reject, just continue with empty output
|
||||||
},
|
resolve();
|
||||||
(_exitCode: number) => {
|
},
|
||||||
resolve();
|
(_exitCode: number) => {
|
||||||
}
|
resolve();
|
||||||
);
|
}
|
||||||
}),
|
);
|
||||||
timeoutPromise
|
}),
|
||||||
]);
|
timeoutPromise
|
||||||
|
]);
|
||||||
// Parse pct list output
|
} catch (error) {
|
||||||
const lines = listOutput.split('\n').filter(line => line.trim());
|
console.error(`Timeout or error getting pct list from server ${(server as any).name}:`, error);
|
||||||
for (const line of lines) {
|
|
||||||
// pct list format: CTID Status Name
|
|
||||||
// Example: "100 running my-container"
|
|
||||||
const parts = line.trim().split(/\s+/);
|
|
||||||
if (parts.length >= 3) {
|
|
||||||
const containerId = parts[0];
|
|
||||||
const status = parts[1];
|
|
||||||
|
|
||||||
if (containerId && status) {
|
|
||||||
// Map pct list status to our status
|
|
||||||
let mappedStatus: 'running' | 'stopped' | 'unknown' = 'unknown';
|
|
||||||
if (status === 'running') {
|
|
||||||
mappedStatus = 'running';
|
|
||||||
} else if (status === 'stopped') {
|
|
||||||
mappedStatus = 'stopped';
|
|
||||||
}
|
|
||||||
|
|
||||||
statusMap[containerId] = mappedStatus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run qm list to get all VM statuses
|
||||||
|
let qmOutput = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
|
server as Server,
|
||||||
|
'qm list',
|
||||||
|
(data: string) => {
|
||||||
|
qmOutput += data;
|
||||||
|
},
|
||||||
|
(error: string) => {
|
||||||
|
console.error(`qm list error on server ${(server as any).name}:`, error);
|
||||||
|
// Don't reject, just continue with empty output
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
(_exitCode: number) => {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
timeoutPromise
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Timeout or error getting qm list from server ${(server as any).name}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse both outputs and combine into statusMap
|
||||||
|
const containerStatuses = parseListStatuses(pctOutput);
|
||||||
|
const vmStatuses = parseListStatuses(qmOutput);
|
||||||
|
|
||||||
|
// Merge both status maps (VMs will overwrite containers if same ID, but that's unlikely)
|
||||||
|
Object.assign(statusMap, containerStatuses, vmStatuses);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error processing server ${(server as any).name}:`, error);
|
console.error(`Error processing server ${(server as any).name}:`, error);
|
||||||
}
|
}
|
||||||
@@ -1207,8 +1478,13 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check container status
|
// Determine if it's a VM or LXC
|
||||||
const statusCommand = `pct status ${scriptData.container_id}`;
|
const vm = await isVM(input.id, scriptData.container_id, scriptData.server_id);
|
||||||
|
|
||||||
|
// Check container status (use qm for VMs, pct for LXC)
|
||||||
|
const statusCommand = vm
|
||||||
|
? `qm status ${scriptData.container_id}`
|
||||||
|
: `pct status ${scriptData.container_id}`;
|
||||||
let statusOutput = '';
|
let statusOutput = '';
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -1305,8 +1581,13 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute control command
|
// Determine if it's a VM or LXC
|
||||||
const controlCommand = `pct ${input.action} ${scriptData.container_id}`;
|
const vm = await isVM(input.id, scriptData.container_id, scriptData.server_id);
|
||||||
|
|
||||||
|
// Execute control command (use qm for VMs, pct for LXC)
|
||||||
|
const controlCommand = vm
|
||||||
|
? `qm ${input.action} ${scriptData.container_id}`
|
||||||
|
: `pct ${input.action} ${scriptData.container_id}`;
|
||||||
let commandOutput = '';
|
let commandOutput = '';
|
||||||
let commandError = '';
|
let commandError = '';
|
||||||
|
|
||||||
@@ -1396,8 +1677,13 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if it's a VM or LXC
|
||||||
|
const vm = await isVM(input.id, scriptData.container_id, scriptData.server_id);
|
||||||
|
|
||||||
// First check if container is running and stop it if necessary
|
// First check if container is running and stop it if necessary
|
||||||
const statusCommand = `pct status ${scriptData.container_id}`;
|
const statusCommand = vm
|
||||||
|
? `qm status ${scriptData.container_id}`
|
||||||
|
: `pct status ${scriptData.container_id}`;
|
||||||
let statusOutput = '';
|
let statusOutput = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1420,8 +1706,10 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// Check if container is running
|
// Check if container is running
|
||||||
if (statusOutput.includes('status: running')) {
|
if (statusOutput.includes('status: running')) {
|
||||||
// Stop the container first
|
// Stop the container first (use qm for VMs, pct for LXC)
|
||||||
const stopCommand = `pct stop ${scriptData.container_id}`;
|
const stopCommand = vm
|
||||||
|
? `qm stop ${scriptData.container_id}`
|
||||||
|
: `pct stop ${scriptData.container_id}`;
|
||||||
let stopOutput = '';
|
let stopOutput = '';
|
||||||
let stopError = '';
|
let stopError = '';
|
||||||
|
|
||||||
@@ -1451,8 +1739,10 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute destroy command
|
// Execute destroy command (use qm for VMs, pct for LXC)
|
||||||
const destroyCommand = `pct destroy ${scriptData.container_id}`;
|
const destroyCommand = vm
|
||||||
|
? `qm destroy ${scriptData.container_id}`
|
||||||
|
: `pct destroy ${scriptData.container_id}`;
|
||||||
let commandOutput = '';
|
let commandOutput = '';
|
||||||
let commandError = '';
|
let commandError = '';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user