feat: Add default and advanced install method selection

- Add ConfigurationModal component for selecting default or advanced installation mode
- Default mode: Uses predefined defaults with minimal user input (hostname from slug, vmbr0, dhcp, etc.)
- Advanced mode: Full configuration modal with all environment variables customizable
- Add support for IPv4 CIDR input when network mode is 'static'
- Add support for IPv6 static address input when IPv6 method is 'static'
- Implement password formatting as '-password <password>' for build.func compatibility
- Auto-enable SSH when password or SSH keys are provided
- Add storage selection dropdowns filtered by server node assignment
- Pass environment variables through entire execution stack (frontend -> WebSocket -> SSH/local execution)
- Add mode environment variable (always set to 'default' for script execution)
- Update ExecutionModeModal to show 'Advanced (Beta)' option
This commit is contained in:
Michel Roegl-Brunner
2025-12-05 15:53:50 +01:00
parent 0ed13fcf0f
commit 7b8c1ebdf1
3 changed files with 42 additions and 39 deletions

View File

@@ -82,6 +82,7 @@ const handle = app.getRequestHandler();
* @property {number} [cloneCount]
* @property {string[]} [hostnames]
* @property {'lxc'|'vm'} [containerType]
* @property {Record<string, string|number|boolean>} [envVars]
*/
class ScriptExecutionHandler {
@@ -421,7 +422,9 @@ class ScriptExecutionHandler {
// Add envVars to environment
if (envVars && typeof envVars === 'object') {
for (const [key, value] of Object.entries(envVars)) {
envWithVars[key] = String(value);
/** @type {Record<string, string>} */
const envRecord = envWithVars;
envRecord[key] = String(value);
}
}

View File

@@ -75,7 +75,7 @@ export function ConfigurationModal({
var_cpu: resources?.cpu ?? 1,
var_ram: resources?.ram ?? 1024,
var_disk: resources?.hdd ?? 4,
var_unprivileged: resources?.privileged === false ? 1 : (resources?.privileged === true ? 0 : 1),
var_unprivileged: script?.privileged === false ? 1 : (script?.privileged === true ? 0 : 1),
// Network defaults
var_net: 'dhcp',
@@ -196,19 +196,19 @@ export function ConfigurationModal({
newErrors.var_ipv6_static = 'Invalid IPv6 address';
}
}
if (!validatePositiveInt(advancedVars.var_cpu)) {
if (!validatePositiveInt(advancedVars.var_cpu as string | number | undefined)) {
newErrors.var_cpu = 'Must be a positive integer';
}
if (!validatePositiveInt(advancedVars.var_ram)) {
if (!validatePositiveInt(advancedVars.var_ram as string | number | undefined)) {
newErrors.var_ram = 'Must be a positive integer';
}
if (!validatePositiveInt(advancedVars.var_disk)) {
if (!validatePositiveInt(advancedVars.var_disk as string | number | undefined)) {
newErrors.var_disk = 'Must be a positive integer';
}
if (advancedVars.var_mtu && !validatePositiveInt(advancedVars.var_mtu)) {
if (advancedVars.var_mtu && !validatePositiveInt(advancedVars.var_mtu as string | number | undefined)) {
newErrors.var_mtu = 'Must be a positive integer';
}
if (advancedVars.var_vlan && !validatePositiveInt(advancedVars.var_vlan)) {
if (advancedVars.var_vlan && !validatePositiveInt(advancedVars.var_vlan as string | number | undefined)) {
newErrors.var_vlan = 'Must be a positive integer';
}
}
@@ -237,7 +237,7 @@ export function ConfigurationModal({
var_cpu: resources?.cpu ?? 1,
var_ram: resources?.ram ?? 1024,
var_disk: resources?.hdd ?? 4,
var_unprivileged: resources?.privileged === false ? 1 : (resources?.privileged === true ? 0 : 1),
var_unprivileged: script?.privileged === false ? 1 : (script?.privileged === true ? 0 : 1),
};
if (containerStorage) {
@@ -385,7 +385,7 @@ export function ConfigurationModal({
<Input
type="number"
min="1"
value={advancedVars.var_cpu ?? ''}
value={typeof advancedVars.var_cpu === 'boolean' ? '' : (advancedVars.var_cpu ?? '')}
onChange={(e) => updateAdvancedVar('var_cpu', parseInt(e.target.value) || 1)}
className={errors.var_cpu ? 'border-destructive' : ''}
/>
@@ -400,7 +400,7 @@ export function ConfigurationModal({
<Input
type="number"
min="1"
value={advancedVars.var_ram ?? ''}
value={typeof advancedVars.var_ram === 'boolean' ? '' : (advancedVars.var_ram ?? '')}
onChange={(e) => updateAdvancedVar('var_ram', parseInt(e.target.value) || 1024)}
className={errors.var_ram ? 'border-destructive' : ''}
/>
@@ -415,7 +415,7 @@ export function ConfigurationModal({
<Input
type="number"
min="1"
value={advancedVars.var_disk ?? ''}
value={typeof advancedVars.var_disk === 'boolean' ? '' : (advancedVars.var_disk ?? '')}
onChange={(e) => updateAdvancedVar('var_disk', parseInt(e.target.value) || 4)}
className={errors.var_disk ? 'border-destructive' : ''}
/>
@@ -428,7 +428,7 @@ export function ConfigurationModal({
Unprivileged
</label>
<select
value={advancedVars.var_unprivileged ?? 1}
value={typeof advancedVars.var_unprivileged === 'boolean' ? (advancedVars.var_unprivileged ? 0 : 1) : (advancedVars.var_unprivileged ?? 1)}
onChange={(e) => updateAdvancedVar('var_unprivileged', parseInt(e.target.value))}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -448,7 +448,7 @@ export function ConfigurationModal({
Network Mode
</label>
<select
value={(typeof advancedVars.var_net === 'string' && advancedVars.var_net.includes('/')) ? 'static' : (advancedVars.var_net ?? 'dhcp')}
value={(typeof advancedVars.var_net === 'string' && advancedVars.var_net.includes('/')) ? 'static' : (typeof advancedVars.var_net === 'boolean' ? 'dhcp' : (advancedVars.var_net ?? 'dhcp'))}
onChange={(e) => {
if (e.target.value === 'static') {
updateAdvancedVar('var_net', 'static');
@@ -492,7 +492,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_brg ?? ''}
value={typeof advancedVars.var_brg === 'boolean' ? '' : String(advancedVars.var_brg ?? '')}
onChange={(e) => updateAdvancedVar('var_brg', e.target.value)}
placeholder="vmbr0"
/>
@@ -503,7 +503,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_gateway ?? ''}
value={typeof advancedVars.var_gateway === 'boolean' ? '' : String(advancedVars.var_gateway ?? '')}
onChange={(e) => updateAdvancedVar('var_gateway', e.target.value)}
placeholder="Auto"
className={errors.var_gateway ? 'border-destructive' : ''}
@@ -517,7 +517,7 @@ export function ConfigurationModal({
IPv6 Method
</label>
<select
value={advancedVars.var_ipv6_method ?? 'none'}
value={typeof advancedVars.var_ipv6_method === 'boolean' ? 'none' : String(advancedVars.var_ipv6_method ?? 'none')}
onChange={(e) => {
updateAdvancedVar('var_ipv6_method', e.target.value);
// Clear IPv6 static when switching away from static
@@ -541,7 +541,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_ipv6_static ?? ''}
value={typeof advancedVars.var_ipv6_static === 'boolean' ? '' : String(advancedVars.var_ipv6_static ?? '')}
onChange={(e) => updateAdvancedVar('var_ipv6_static', e.target.value)}
placeholder="2001:db8::1/64"
className={errors.var_ipv6_static ? 'border-destructive' : ''}
@@ -558,7 +558,7 @@ export function ConfigurationModal({
<Input
type="number"
min="1"
value={advancedVars.var_vlan ?? ''}
value={typeof advancedVars.var_vlan === 'boolean' ? '' : String(advancedVars.var_vlan ?? '')}
onChange={(e) => updateAdvancedVar('var_vlan', e.target.value ? parseInt(e.target.value) : '')}
placeholder="None"
className={errors.var_vlan ? 'border-destructive' : ''}
@@ -574,7 +574,7 @@ export function ConfigurationModal({
<Input
type="number"
min="1"
value={advancedVars.var_mtu ?? ''}
value={typeof advancedVars.var_mtu === 'boolean' ? '' : String(advancedVars.var_mtu ?? '')}
onChange={(e) => updateAdvancedVar('var_mtu', e.target.value ? parseInt(e.target.value) : 1500)}
placeholder="1500"
className={errors.var_mtu ? 'border-destructive' : ''}
@@ -589,7 +589,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_mac ?? ''}
value={typeof advancedVars.var_mac === 'boolean' ? '' : String(advancedVars.var_mac ?? '')}
onChange={(e) => updateAdvancedVar('var_mac', e.target.value)}
placeholder="Auto"
className={errors.var_mac ? 'border-destructive' : ''}
@@ -604,7 +604,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_ns ?? ''}
value={typeof advancedVars.var_ns === 'boolean' ? '' : String(advancedVars.var_ns ?? '')}
onChange={(e) => updateAdvancedVar('var_ns', e.target.value)}
placeholder="Auto"
className={errors.var_ns ? 'border-destructive' : ''}
@@ -626,7 +626,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_hostname ?? ''}
value={typeof advancedVars.var_hostname === 'boolean' ? '' : String(advancedVars.var_hostname ?? '')}
onChange={(e) => updateAdvancedVar('var_hostname', e.target.value)}
placeholder={slug}
/>
@@ -637,7 +637,7 @@ export function ConfigurationModal({
</label>
<Input
type="password"
value={advancedVars.var_pw ?? ''}
value={typeof advancedVars.var_pw === 'boolean' ? '' : String(advancedVars.var_pw ?? '')}
onChange={(e) => updateAdvancedVar('var_pw', e.target.value)}
placeholder="Random (empty = auto-login)"
/>
@@ -648,7 +648,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_tags ?? ''}
value={typeof advancedVars.var_tags === 'boolean' ? '' : String(advancedVars.var_tags ?? '')}
onChange={(e) => updateAdvancedVar('var_tags', e.target.value)}
placeholder="community-script"
/>
@@ -665,7 +665,7 @@ export function ConfigurationModal({
Enable SSH
</label>
<select
value={advancedVars.var_ssh ?? 'no'}
value={typeof advancedVars.var_ssh === 'boolean' ? (advancedVars.var_ssh ? 'yes' : 'no') : String(advancedVars.var_ssh ?? 'no')}
onChange={(e) => updateAdvancedVar('var_ssh', e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -679,7 +679,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_ssh_authorized_key ?? ''}
value={typeof advancedVars.var_ssh_authorized_key === 'boolean' ? '' : String(advancedVars.var_ssh_authorized_key ?? '')}
onChange={(e) => updateAdvancedVar('var_ssh_authorized_key', e.target.value)}
placeholder="ssh-rsa AAAA..."
/>
@@ -696,7 +696,7 @@ export function ConfigurationModal({
Nesting (Docker)
</label>
<select
value={advancedVars.var_nesting ?? 1}
value={typeof advancedVars.var_nesting === 'boolean' ? 1 : (advancedVars.var_nesting ?? 1)}
onChange={(e) => updateAdvancedVar('var_nesting', parseInt(e.target.value))}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -709,7 +709,7 @@ export function ConfigurationModal({
FUSE
</label>
<select
value={advancedVars.var_fuse ?? 0}
value={typeof advancedVars.var_fuse === 'boolean' ? 0 : (advancedVars.var_fuse ?? 0)}
onChange={(e) => updateAdvancedVar('var_fuse', parseInt(e.target.value))}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -722,7 +722,7 @@ export function ConfigurationModal({
Keyctl
</label>
<select
value={advancedVars.var_keyctl ?? 0}
value={typeof advancedVars.var_keyctl === 'boolean' ? 0 : (advancedVars.var_keyctl ?? 0)}
onChange={(e) => updateAdvancedVar('var_keyctl', parseInt(e.target.value))}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -735,7 +735,7 @@ export function ConfigurationModal({
Mknod
</label>
<select
value={advancedVars.var_mknod ?? 0}
value={typeof advancedVars.var_mknod === 'boolean' ? 0 : (advancedVars.var_mknod ?? 0)}
onChange={(e) => updateAdvancedVar('var_mknod', parseInt(e.target.value))}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -749,7 +749,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_mount_fs ?? ''}
value={typeof advancedVars.var_mount_fs === 'boolean' ? '' : String(advancedVars.var_mount_fs ?? '')}
onChange={(e) => updateAdvancedVar('var_mount_fs', e.target.value)}
placeholder="nfs,cifs"
/>
@@ -759,7 +759,7 @@ export function ConfigurationModal({
Protection
</label>
<select
value={advancedVars.var_protection ?? 'no'}
value={typeof advancedVars.var_protection === 'boolean' ? (advancedVars.var_protection ? 'yes' : 'no') : String(advancedVars.var_protection ?? 'no')}
onChange={(e) => updateAdvancedVar('var_protection', e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -780,7 +780,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={advancedVars.var_timezone ?? ''}
value={typeof advancedVars.var_timezone === 'boolean' ? '' : String(advancedVars.var_timezone ?? '')}
onChange={(e) => updateAdvancedVar('var_timezone', e.target.value)}
placeholder="System"
/>
@@ -790,7 +790,7 @@ export function ConfigurationModal({
Verbose
</label>
<select
value={advancedVars.var_verbose ?? 'no'}
value={typeof advancedVars.var_verbose === 'boolean' ? (advancedVars.var_verbose ? 'yes' : 'no') : String(advancedVars.var_verbose ?? 'no')}
onChange={(e) => updateAdvancedVar('var_verbose', e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -803,7 +803,7 @@ export function ConfigurationModal({
APT Cacher
</label>
<select
value={advancedVars.var_apt_cacher ?? 'no'}
value={typeof advancedVars.var_apt_cacher === 'boolean' ? (advancedVars.var_apt_cacher ? 'yes' : 'no') : String(advancedVars.var_apt_cacher ?? 'no')}
onChange={(e) => updateAdvancedVar('var_apt_cacher', e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -817,7 +817,7 @@ export function ConfigurationModal({
</label>
<Input
type="text"
value={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)}
placeholder="192.168.1.10"
className={errors.var_apt_cacher_ip ? 'border-destructive' : ''}
@@ -838,7 +838,7 @@ export function ConfigurationModal({
Container Storage
</label>
<select
value={advancedVars.var_container_storage ?? ''}
value={typeof advancedVars.var_container_storage === 'boolean' ? '' : String(advancedVars.var_container_storage ?? '')}
onChange={(e) => updateAdvancedVar('var_container_storage', e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>
@@ -860,7 +860,7 @@ export function ConfigurationModal({
Template Storage
</label>
<select
value={advancedVars.var_template_storage ?? ''}
value={typeof advancedVars.var_template_storage === 'boolean' ? '' : String(advancedVars.var_template_storage ?? '')}
onChange={(e) => updateAdvancedVar('var_template_storage', e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none"
>

View File

@@ -195,7 +195,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName, scr
size="default"
className="flex-1"
>
Advanced
Advanced (Beta)
</Button>
</div>
</div>