Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4628e67e5c | ||
|
|
578fa28461 | ||
|
|
9e6154b0de | ||
|
|
d29f71a92f | ||
|
|
aea14cda7e | ||
|
|
4893ccda6e |
@@ -199,6 +199,17 @@ export function ConfigurationModal({
|
|||||||
return !isNaN(num) && num > 0;
|
return !isNaN(num) && num > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateHostname = (hostname: string): boolean => {
|
||||||
|
if (!hostname || hostname.length > 253) return false;
|
||||||
|
const label = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
||||||
|
const labels = hostname.split('.');
|
||||||
|
return labels.length >= 1 && labels.every(l => l.length >= 1 && l.length <= 63 && label.test(l));
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAptCacherAddress = (value: string): boolean => {
|
||||||
|
return validateIPv4(value) || validateHostname(value);
|
||||||
|
};
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -216,8 +227,8 @@ export function ConfigurationModal({
|
|||||||
if (advancedVars.var_ns && !validateIPv4(advancedVars.var_ns as string)) {
|
if (advancedVars.var_ns && !validateIPv4(advancedVars.var_ns as string)) {
|
||||||
newErrors.var_ns = 'Invalid IPv4 address';
|
newErrors.var_ns = 'Invalid IPv4 address';
|
||||||
}
|
}
|
||||||
if (advancedVars.var_apt_cacher_ip && !validateIPv4(advancedVars.var_apt_cacher_ip as string)) {
|
if (advancedVars.var_apt_cacher_ip && !validateAptCacherAddress(advancedVars.var_apt_cacher_ip as string)) {
|
||||||
newErrors.var_apt_cacher_ip = 'Invalid IPv4 address';
|
newErrors.var_apt_cacher_ip = 'Invalid IPv4 address or hostname';
|
||||||
}
|
}
|
||||||
// Validate IPv4 CIDR if network mode is static
|
// Validate IPv4 CIDR if network mode is static
|
||||||
const netValue = advancedVars.var_net;
|
const netValue = advancedVars.var_net;
|
||||||
@@ -904,13 +915,13 @@ export function ConfigurationModal({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
APT Cacher IP
|
APT Cacher host or IP
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={typeof advancedVars.var_apt_cacher_ip === 'boolean' ? '' : String(advancedVars.var_apt_cacher_ip ?? '')}
|
value={typeof advancedVars.var_apt_cacher_ip === 'boolean' ? '' : String(advancedVars.var_apt_cacher_ip ?? '')}
|
||||||
onChange={(e) => updateAdvancedVar('var_apt_cacher_ip', e.target.value)}
|
onChange={(e) => updateAdvancedVar('var_apt_cacher_ip', e.target.value)}
|
||||||
placeholder="192.168.1.10"
|
placeholder="192.168.1.10 or apt-cacher.internal"
|
||||||
className={errors.var_apt_cacher_ip ? 'border-destructive' : ''}
|
className={errors.var_apt_cacher_ip ? 'border-destructive' : ''}
|
||||||
/>
|
/>
|
||||||
{errors.var_apt_cacher_ip && (
|
{errors.var_apt_cacher_ip && (
|
||||||
|
|||||||
@@ -270,22 +270,21 @@ export function PBSCredentialsModal({
|
|||||||
htmlFor="pbs-fingerprint"
|
htmlFor="pbs-fingerprint"
|
||||||
className="text-foreground mb-1 block text-sm font-medium"
|
className="text-foreground mb-1 block text-sm font-medium"
|
||||||
>
|
>
|
||||||
Fingerprint <span className="text-error">*</span>
|
Fingerprint
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="pbs-fingerprint"
|
id="pbs-fingerprint"
|
||||||
value={pbsFingerprint}
|
value={pbsFingerprint}
|
||||||
onChange={(e) => setPbsFingerprint(e.target.value)}
|
onChange={(e) => setPbsFingerprint(e.target.value)}
|
||||||
required
|
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="bg-card text-foreground placeholder-muted-foreground focus:ring-ring focus:border-ring border-border w-full rounded-md border px-3 py-2 shadow-sm focus:ring-2 focus:outline-none"
|
className="bg-card text-foreground placeholder-muted-foreground focus:ring-ring focus:border-ring border-border w-full rounded-md border px-3 py-2 shadow-sm focus:ring-2 focus:outline-none"
|
||||||
placeholder="e.g., 7b:e5:87:38:5e:16:05:d1:12:22:7f:73:d2:e2:d0:cf:8c:cb:28:e2:74:0c:78:91:1a:71:74:2e:79:20:5a:02"
|
placeholder="e.g., 7b:e5:87:38:5e:16:05:d1:12:22:7f:73:d2:e2:d0:cf:8c:cb:28:e2:74:0c:78:91:1a:71:74:2e:79:20:5a:02"
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
Server fingerprint for auto-acceptance. You can find this on
|
Leave empty if PBS uses a trusted CA (e.g. Let's Encrypt).
|
||||||
your PBS dashboard by clicking the "Show Fingerprint"
|
For self-signed certificates, enter the server fingerprint from
|
||||||
button.
|
the PBS dashboard ("Show Fingerprint").
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2068,32 +2068,72 @@ export const installedScriptsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the script's interface_port from metadata (prioritize metadata over existing database values)
|
// Resolve app slug from /usr/bin/update (community-scripts) when available; else from hostname/suffix.
|
||||||
let detectedPort = 80; // Default fallback
|
let slugFromUpdate: string | null = null;
|
||||||
|
try {
|
||||||
|
const updateCommand = `pct exec ${scriptData.container_id} -- cat /usr/bin/update 2>/dev/null`;
|
||||||
|
let updateOutput = '';
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
void sshExecutionService.executeCommand(
|
||||||
|
server as Server,
|
||||||
|
updateCommand,
|
||||||
|
(data: string) => { updateOutput += data; },
|
||||||
|
() => {},
|
||||||
|
() => resolve()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const ctSlugMatch = /ct\/([a-zA-Z0-9_.-]+)\.sh/.exec(updateOutput);
|
||||||
|
if (ctSlugMatch?.[1]) {
|
||||||
|
slugFromUpdate = ctSlugMatch[1].trim().toLowerCase();
|
||||||
|
console.log('🔍 Slug from /usr/bin/update:', slugFromUpdate);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Container may not be from community-scripts; use hostname fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the script's interface_port from metadata. Primary: slug from /usr/bin/update; fallback: hostname/suffix.
|
||||||
|
let detectedPort = 80; // Default fallback
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Import localScriptsService to get script metadata
|
|
||||||
const { localScriptsService } = await import('~/server/services/localScripts');
|
const { localScriptsService } = await import('~/server/services/localScripts');
|
||||||
|
|
||||||
// Get all scripts and find the one matching our script name
|
|
||||||
const allScripts = await localScriptsService.getAllScripts();
|
const allScripts = await localScriptsService.getAllScripts();
|
||||||
|
|
||||||
// Extract script slug from script_name (remove .sh extension)
|
const nameFromHostname = scriptData.script_name.replace(/\.sh$/, '').toLowerCase();
|
||||||
const scriptSlug = scriptData.script_name.replace(/\.sh$/, '');
|
|
||||||
console.log('🔍 Looking for script with slug:', scriptSlug);
|
// Primary: slug from /usr/bin/update (community-scripts)
|
||||||
|
let scriptMetadata =
|
||||||
const scriptMetadata = allScripts.find(script => script.slug === scriptSlug);
|
slugFromUpdate != null
|
||||||
|
? allScripts.find((s) => s.slug === slugFromUpdate)
|
||||||
|
: undefined;
|
||||||
|
if (scriptMetadata) {
|
||||||
|
console.log('🔍 Using slug from /usr/bin/update for metadata:', scriptMetadata.slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: exact hostname then hostname ends with slug (longest wins)
|
||||||
|
if (!scriptMetadata) {
|
||||||
|
scriptMetadata = allScripts.find((script) => script.slug === nameFromHostname);
|
||||||
|
if (!scriptMetadata) {
|
||||||
|
const suffixMatches = allScripts.filter((script) => nameFromHostname.endsWith(script.slug));
|
||||||
|
scriptMetadata =
|
||||||
|
suffixMatches.length > 0
|
||||||
|
? suffixMatches.reduce((a, b) => (a.slug.length >= b.slug.length ? a : b))
|
||||||
|
: undefined;
|
||||||
|
if (scriptMetadata) {
|
||||||
|
console.log('🔍 Matched metadata by slug suffix in hostname:', scriptMetadata.slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (scriptMetadata?.interface_port) {
|
if (scriptMetadata?.interface_port) {
|
||||||
detectedPort = scriptMetadata.interface_port;
|
detectedPort = scriptMetadata.interface_port;
|
||||||
console.log('📋 Found interface_port in metadata:', detectedPort);
|
console.log('📋 Found interface_port in metadata:', detectedPort);
|
||||||
} else {
|
} else {
|
||||||
console.log('📋 No interface_port found in metadata, using default port 80');
|
console.log('📋 No interface_port found in metadata, using default port 80');
|
||||||
detectedPort = 80; // Default to port 80 if no metadata port found
|
detectedPort = 80;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('⚠️ Error getting script metadata, using default port 80:', error);
|
console.log('⚠️ Error getting script metadata, using default port 80:', error);
|
||||||
detectedPort = 80; // Default to port 80 if metadata lookup fails
|
detectedPort = 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🎯 Final detected port:', detectedPort);
|
console.log('🎯 Final detected port:', detectedPort);
|
||||||
|
|||||||
@@ -327,13 +327,16 @@ class BackupService {
|
|||||||
// PBS supports PBS_PASSWORD and PBS_REPOSITORY environment variables for non-interactive login
|
// PBS supports PBS_PASSWORD and PBS_REPOSITORY environment variables for non-interactive login
|
||||||
const repository = `root@pam@${pbsIp}:${pbsDatastore}`;
|
const repository = `root@pam@${pbsIp}:${pbsDatastore}`;
|
||||||
|
|
||||||
// Escape password for shell safety (single quotes)
|
// Escape password and fingerprint for shell safety (single quotes)
|
||||||
const escapedPassword = credential.pbs_password.replace(/'/g, "'\\''");
|
const escapedPassword = credential.pbs_password.replace(/'/g, "'\\''");
|
||||||
|
const fingerprint = credential.pbs_fingerprint?.trim() ?? '';
|
||||||
// Use PBS_PASSWORD environment variable for non-interactive authentication
|
const escapedFingerprint = fingerprint ? fingerprint.replace(/'/g, "'\\''") : '';
|
||||||
// Auto-accept fingerprint by piping "y" to stdin
|
const envParts = [`PBS_PASSWORD='${escapedPassword}'`, `PBS_REPOSITORY='${repository}'`];
|
||||||
// PBS will use PBS_PASSWORD env var if available, avoiding interactive prompt
|
if (escapedFingerprint) {
|
||||||
const fullCommand = `echo "y" | PBS_PASSWORD='${escapedPassword}' PBS_REPOSITORY='${repository}' timeout 10 proxmox-backup-client login --repository ${repository} 2>&1`;
|
envParts.push(`PBS_FINGERPRINT='${escapedFingerprint}'`);
|
||||||
|
}
|
||||||
|
const envStr = envParts.join(' ');
|
||||||
|
const fullCommand = `${envStr} timeout 10 proxmox-backup-client login --repository ${repository} 2>&1`;
|
||||||
|
|
||||||
console.log(`[BackupService] Logging into PBS: ${repository}`);
|
console.log(`[BackupService] Logging into PBS: ${repository}`);
|
||||||
|
|
||||||
@@ -419,9 +422,12 @@ class BackupService {
|
|||||||
|
|
||||||
// Build full repository string: root@pam@<IP>:<DATASTORE>
|
// Build full repository string: root@pam@<IP>:<DATASTORE>
|
||||||
const repository = `root@pam@${pbsIp}:${pbsDatastore}`;
|
const repository = `root@pam@${pbsIp}:${pbsDatastore}`;
|
||||||
|
const fingerprint = credential.pbs_fingerprint?.trim() ?? '';
|
||||||
|
const escapedFingerprint = fingerprint ? fingerprint.replace(/'/g, "'\\''") : '';
|
||||||
|
const snapshotEnvParts = escapedFingerprint ? [`PBS_FINGERPRINT='${escapedFingerprint}'`] : [];
|
||||||
|
const snapshotEnvStr = snapshotEnvParts.length ? snapshotEnvParts.join(' ') + ' ' : '';
|
||||||
// Use correct command: snapshot list ct/<CT_ID> --repository <full_repo_string>
|
// Use correct command: snapshot list ct/<CT_ID> --repository <full_repo_string>
|
||||||
const command = `timeout 30 proxmox-backup-client snapshot list ct/${ctId} --repository ${repository} 2>&1 || echo "PBS_ERROR"`;
|
const command = `${snapshotEnvStr}timeout 30 proxmox-backup-client snapshot list ct/${ctId} --repository ${repository} 2>&1 || echo "PBS_ERROR"`;
|
||||||
let output = '';
|
let output = '';
|
||||||
|
|
||||||
console.log(`[BackupService] Discovering PBS backups for CT ${ctId} on repository ${repository}`);
|
console.log(`[BackupService] Discovering PBS backups for CT ${ctId} on repository ${repository}`);
|
||||||
|
|||||||
@@ -250,9 +250,16 @@ class RestoreService {
|
|||||||
const targetFolder = `/var/lib/vz/dump/vzdump-lxc-${ctId}-${snapshotNameForPath}`;
|
const targetFolder = `/var/lib/vz/dump/vzdump-lxc-${ctId}-${snapshotNameForPath}`;
|
||||||
const targetTar = `${targetFolder}.tar`;
|
const targetTar = `${targetFolder}.tar`;
|
||||||
|
|
||||||
// Use PBS_PASSWORD env var and add timeout for long downloads
|
// Use PBS_PASSWORD env var and add timeout for long downloads; PBS_FINGERPRINT when set for cert validation
|
||||||
const escapedPassword = credential.pbs_password.replace(/'/g, "'\\''");
|
const escapedPassword = credential.pbs_password.replace(/'/g, "'\\''");
|
||||||
const restoreCommand = `PBS_PASSWORD='${escapedPassword}' PBS_REPOSITORY='${repository}' timeout 300 proxmox-backup-client restore "${snapshotPath}" root.pxar "${targetFolder}" --repository '${repository}' 2>&1`;
|
const fingerprint = credential.pbs_fingerprint?.trim() ?? '';
|
||||||
|
const escapedFingerprint = fingerprint ? fingerprint.replace(/'/g, "'\\''") : '';
|
||||||
|
const restoreEnvParts = [`PBS_PASSWORD='${escapedPassword}'`, `PBS_REPOSITORY='${repository}'`];
|
||||||
|
if (escapedFingerprint) {
|
||||||
|
restoreEnvParts.push(`PBS_FINGERPRINT='${escapedFingerprint}'`);
|
||||||
|
}
|
||||||
|
const restoreEnvStr = restoreEnvParts.join(' ');
|
||||||
|
const restoreCommand = `${restoreEnvStr} timeout 300 proxmox-backup-client restore "${snapshotPath}" root.pxar "${targetFolder}" --repository '${repository}' 2>&1`;
|
||||||
|
|
||||||
let output = '';
|
let output = '';
|
||||||
let exitCode = 0;
|
let exitCode = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user