297 lines
28 KiB
JavaScript
297 lines
28 KiB
JavaScript
/* Graphical installation settings. The server validates the resolved host configuration. */
|
||
const installationForm = (() => {
|
||
const clone = value => JSON.parse(JSON.stringify(value));
|
||
const object = (value, label) => {
|
||
if (value === undefined) return {};
|
||
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label}: Erwartet werden benannte Einstellungen.`);
|
||
return value;
|
||
};
|
||
const keyboards = [
|
||
['de','Deutsch'],['de-ch','Deutsch (Schweiz)'],['dk','Dänisch'],['en-gb','Englisch (Großbritannien)'],
|
||
['en-us','Englisch (USA)'],['es','Spanisch'],['fi','Finnisch'],['fr','Französisch'],
|
||
['fr-be','Französisch (Belgien)'],['fr-ca','Französisch (Kanada)'],['fr-ch','Französisch (Schweiz)'],
|
||
['hu','Ungarisch'],['is','Isländisch'],['it','Italienisch'],['jp','Japanisch'],['lt','Litauisch'],
|
||
['mk','Mazedonisch'],['nl','Niederländisch'],['no','Norwegisch'],['pl','Polnisch'],
|
||
['pt','Portugiesisch'],['pt-br','Portugiesisch (Brasilien)'],['se','Schwedisch'],['si','Slowenisch'],['tr','Türkisch']
|
||
];
|
||
const countryCodes = 'AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW'.split(' ');
|
||
const countries = (() => {
|
||
const names = typeof Intl.DisplayNames === 'function' ? new Intl.DisplayNames(['de'],{type:'region'}) : null;
|
||
return countryCodes.map(code => [code.toLowerCase(),names?.of(code) || ({DE:'Deutschland',AT:'Österreich',CH:'Schweiz'}[code] || code)])
|
||
.sort((a,b) => a[1].localeCompare(b[1],'de'));
|
||
})();
|
||
const timezones = (() => {
|
||
const regions = {Africa:'Afrika',America:'Amerika',Antarctica:'Antarktis',Arctic:'Arktis',Asia:'Asien',Atlantic:'Atlantik',Australia:'Australien',Europe:'Europa',Indian:'Indischer Ozean',Pacific:'Pazifik'};
|
||
const supported = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('timeZone') : [];
|
||
return [...new Set(['Europe/Berlin','UTC',...supported])].map(zone => [zone,zone === 'UTC' ? 'UTC (koordinierte Weltzeit)' : zone.split('/').map((part,index) => index === 0 ? regions[part] || part : part.replaceAll('_',' ')).join(' / ')])
|
||
.sort((a,b) => a[1].localeCompare(b[1],'de'));
|
||
})();
|
||
const interfaces = [
|
||
['ID_NET_NAME_MAC','Name aus MAC-Adresse'], ['ID_NET_NAME_PATH','Name aus Gerätepfad'],
|
||
['ID_NET_NAME_SLOT','Name des Steckplatzes'], ['ID_NET_NAME_ONBOARD','Name der Onboard-Schnittstelle'],
|
||
['INTERFACE','Schnittstellenname'], ['ID_PATH','Gerätepfad'], ['ID_NET_DRIVER','Treiber']
|
||
];
|
||
const raids = [['raid0','RAID0 / Einzelplatte'],['raid1','RAID1 / Spiegel'],['raid10','RAID10'],['raidz-1','RAIDZ1'],['raidz-2','RAIDZ2'],['raidz-3','RAIDZ3']];
|
||
const zfsNumbers = [['ashift','Sektorgröße (ashift)',9,16,true],['arc-max','Maximaler ARC in MiB',64,1048576,true],['copies','Datenkopien',1,3,true],['hdsize','Verwendete Plattengröße in GiB',2,1000000,false]];
|
||
const lvmNumbers = [['hdsize','Verwendete Plattengröße in GiB',2],['swapsize','Swap-Größe in GiB',0],['maxroot','Maximale Root-Größe in GiB',2],['maxvz','Maximale Daten-Größe in GiB',0],['minfree','Freier Platz in GiB',0]];
|
||
const optionsFor = (entries, current, blank = 'Nicht festlegen') => {
|
||
const items = entries.map(item => Array.isArray(item) ? {value:item[0],label:item[1]} : {value:item,label:item});
|
||
if (current !== undefined && current !== '' && !items.some(item => String(item.value) === String(current))) items.push({value:current,label:`Bestehender Wert: ${current}`});
|
||
return [{value:'',label:blank}, ...items];
|
||
};
|
||
function root(container, prefix) {
|
||
const nodes = [...(container.matches?.('[data-installation-editor]') ? [container] : []), ...container.querySelectorAll('[data-installation-editor]')];
|
||
const found = nodes.find(node => node.dataset.installationEditor === prefix);
|
||
if (!found) throw new Error('Das Installationsformular wurde nicht gefunden. Bitte erneut öffnen.');
|
||
return found;
|
||
}
|
||
function text(value, label) {
|
||
if (value === undefined) return '';
|
||
if (typeof value !== 'string') throw new Error(`${label}: Der gespeicherte Wert muss Text sein.`);
|
||
return value;
|
||
}
|
||
function strings(value, label) {
|
||
if (value === undefined) return [];
|
||
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) throw new Error(`${label}: Die gespeicherten Einträge müssen Text sein.`);
|
||
return value;
|
||
}
|
||
function numeric(value, label) {
|
||
if (value === undefined) return '';
|
||
if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`${label}: Der gespeicherte Wert muss eine Zahl sein.`);
|
||
return value;
|
||
}
|
||
function networkRow(prefix, key = 'ID_NET_NAME_MAC', value = '') {
|
||
const mac = key === 'ID_NET_NAME_MAC' && (!value || /^enx[0-9a-f]{12}$/.test(value));
|
||
const known = interfaces.some(item => item[0] === key);
|
||
const selected = mac ? '__mac' : known ? key : '__custom';
|
||
if (mac && value) value = value.slice(3).match(/../g).join(':');
|
||
return `<div class="form-grid full" data-installation-network-row>
|
||
${field(`${prefix}.interface-key`,'Schnittstelle erkennen über',selected,{type:'select',options:optionsFor([['__mac','MAC-Adresse'],...interfaces,['__custom','Anderes Merkmal']],selected)})}
|
||
${field(`${prefix}.interface-value`,'MAC-Adresse oder erwarteter Wert',text(value,'Schnittstellenmerkmal'),{placeholder:mac?'02:00:00:00:00:01':'Erwarteter Merkmalswert'})}
|
||
<div class="full" data-installation-custom-key${known?' hidden':''}>${field(`${prefix}.interface-custom`,'Name des anderen Merkmals',known?'':key,{full:true})}</div>
|
||
<button type="button" class="button small" data-installation-remove-interface>Merkmal entfernen</button>
|
||
</div>`;
|
||
}
|
||
function render(values = {}, options = {}) {
|
||
const prefix = options.prefix || 'installation', override = Boolean(options.override);
|
||
values = clone(object(values,'Installationskonfiguration'));
|
||
const fresh = !override && Object.keys(values).length === 0;
|
||
if (fresh) values = {global:{keyboard:'de',country:'de',timezone:'Europe/Berlin'},network:{source:'from-answer'},disk_setup:{filesystem:'zfs',selection:'all',zfs:{raid:'raid0'}}};
|
||
const global = object(values.global,'Allgemeine Einstellungen'), network = object(values.network,'Managementnetz');
|
||
const disks = object(values.disk_setup,'Datenträger'), zfs = object(disks.zfs,'ZFS'), lvm = object(disks.lvm,'LVM');
|
||
const netFilter = object(network.filter,'Schnittstellenmerkmale'), diskFilter = object(disks.filter,'Datenträgerfilter');
|
||
if (Object.keys(diskFilter).length > 1) throw new Error('Die Datenträgerauswahl enthält mehrere Filter. Es wird genau ein Seriennummern- oder WWN-Filter unterstützt.');
|
||
if (global['reboot-on-error'] !== undefined && typeof global['reboot-on-error'] !== 'boolean') throw new Error('Neustart bei Fehler: Der gespeicherte Wert muss Ja oder Nein sein.');
|
||
const input = (name,label,value,settings={}) => field(`${prefix}.${name}`,label,text(value,label),{...settings});
|
||
const select = (name,label,value,entries,settings={}) => input(name,label,value,{type:'select',options:optionsFor(entries,value,override?'Vom Profil übernehmen':'Nicht festlegen'),...settings});
|
||
const number = (name,label,value,min,max,integer=false) => field(`${prefix}.${name}`,label,numeric(value,label),{type:'number',min,max,hint:'Leer lassen: vorhandene Vorgabe verwenden.'}).replace('type="number"',`type="number" step="${integer?'1':'any'}"`);
|
||
const section = (name,title,body) => `<section class="full installation-section" data-installation-section="${esc(name)}">
|
||
${override?field(`${prefix}.enable.${name}`,`${title} für diesen Host festlegen`,Object.hasOwn(values,name),{type:'checkbox',full:true}):`<h3 class="section-label">${esc(title)}</h3>`}
|
||
<div class="form-grid" data-installation-section-body>${body}</div></section>`;
|
||
const required = !override;
|
||
const secretId = text(values.root_secret_id,'Root-Geheimnis');
|
||
const secrets = (options.secrets || []).map(secret => [secret.id, secret.name || secret.id]);
|
||
if (secretId && !secrets.some(secret => secret[0] === secretId)) secrets.push([secretId,`Bisherige Referenz (${secretId})`]);
|
||
const diskMode = disks.selection !== undefined ? disks.selection : Object.keys(disks).some(key => ['filter','filter_match','expected_serials','expected_count'].includes(key)) ? 'filtered' : '';
|
||
const diskKey = Object.keys(diskFilter)[0] || (override ? '' : 'ID_SERIAL_SHORT');
|
||
const diskPattern = diskKey ? diskFilter[diskKey] : undefined;
|
||
const networkRows = Object.entries(netFilter).map(([key,value]) => networkRow(prefix,key,value)).join('') || (override?'':networkRow(prefix));
|
||
const reboot = global['reboot-on-error'] === undefined ? '' : String(global['reboot-on-error']);
|
||
const general = select('global.keyboard','Tastatur',global.keyboard,keyboards,{required})
|
||
+ select('global.country','Land',global.country,countries,{required})
|
||
+ select('global.timezone','Zeitzone',global.timezone,timezones,{required})
|
||
+ input('global.mailto','E-Mail für Systemmeldungen',global.mailto,{required,type:'email'})
|
||
+ input('global.fqdn','Hostname-Vorgabe (optional)',global.fqdn,{hint:'Der tatsächlich zugewiesene Server liefert seinen vollständigen Hostnamen.'})
|
||
+ select('global.reboot-on-error','Bei Installationsfehler neu starten',reboot,[['false','Nein'],['true','Ja']])
|
||
+ dataEditor(`${prefix}.global.root-ssh-keys`,strings(global['root-ssh-keys'],'SSH-Schlüssel'),{label:'Öffentliche SSH-Schlüssel für root',schema:{type:'array',description:'Für jeden öffentlichen Schlüssel einen Eintrag hinzufügen. Keine privaten Schlüssel hinterlegen.',items:{type:'string',title:'Öffentlicher SSH-Schlüssel',minLength:1}}});
|
||
const networking = select('network.source','Netzwerkkonfiguration',network.source,[['from-answer','Festes Managementnetz aus diesem Profil']],{required})
|
||
+ input('network.gateway','Standardgateway',network.gateway,{required})
|
||
+ input('network.dns','DNS-Server',network.dns,{required})
|
||
+ input('network.cidr','Management-IP mit Präfix (optional)',network.cidr,{hint:'Die tatsächliche IP mit Präfix kommt aus dem Serverinventar.'})
|
||
+ `<div class="full"><h4>Management-Schnittstelle</h4><p class="small-text muted">Ein oder mehrere Merkmale der gewünschten Netzwerkkarte. Der Installer verwendet diese zur Auswahl.</p><div data-installation-network-rows>${networkRows}</div><button type="button" class="button small" data-installation-add-interface>Merkmal hinzufügen</button></div>`;
|
||
const filtering = select('disk.filter.key','Datenträger erkennen über',diskKey,[['ID_SERIAL_SHORT','Seriennummer'],['ID_SERIAL','Vollständige Serienkennung'],['ID_WWN','WWN']],{required})
|
||
+ input('disk.filter.value','Seriennummer oder Suchmuster',diskPattern,{required,hint:'Ein konkreter Wert oder ein Muster für die unten bestätigten Kennungen.'})
|
||
+ select('disk.filter_match','Filterverknüpfung',disks.filter_match,[['all','Alle Merkmale'],['any','Mindestens ein Merkmal']])
|
||
+ number('disk.expected_count','Anzahl bestätigter Datenträger',disks.expected_count,1,16,true)
|
||
+ dataEditor(`${prefix}.disk.expected_serials`,strings(disks.expected_serials,'Datenträgerkennungen'),{label:'Bestätigte Seriennummern oder WWNs',schema:{type:'array',description:'Jeden bestätigten Datenträger als eigenen Eintrag hinzufügen. Die Anzahl wird aus der Liste übernommen, wenn das Anzahlfeld leer bleibt; die tatsächliche Hardware wird dadurch nicht gezählt.',items:{type:'string',title:'Seriennummer oder WWN',minLength:1}}});
|
||
const zfsFields = select('disk.zfs.raid','ZFS-Verbund',zfs.raid,raids,{required})
|
||
+ zfsNumbers.map(([key,label,min,max,integer]) => number(`disk.zfs.${key}`,label,zfs[key],min,max,integer)).join('')
|
||
+ select('disk.zfs.checksum','Prüfsummen',zfs.checksum,[['on','Standard'],['fletcher4','Fletcher4'],['sha256','SHA-256']])
|
||
+ select('disk.zfs.compress','Kompression',zfs.compress,[['on','Standard'],['off','Aus'],['lzjb','LZJB'],['lz4','LZ4'],['zle','ZLE'],['gzip','Gzip'],['zstd','Zstandard']]);
|
||
const storage = select('disk.filesystem','Dateisystem',disks.filesystem,[['zfs','ZFS'],['ext4','ext4 / LVM'],['xfs','XFS / LVM']],{required})
|
||
+ select('disk.mode','Datenträgerauswahl',diskMode,[['all','Automatisch – Server mit einer Zielplatte'],['filtered','Über Seriennummer oder WWN auswählen']],{required})
|
||
+ `<div class="full alert alert-info" data-installation-all-note>Für Server mit genau einer Zielplatte: Es werden alle vom Installer erkannten Zielplatten verwendet. Seriennummer und Gerätename sind nicht erforderlich. Die tatsächliche Anzahl wird nicht geprüft.</div>`
|
||
+ `<div class="form-grid full" data-installation-filter-fields>${filtering}</div>`
|
||
+ input('disk.inventory_evidence','Referenz zur Hardwareprüfung',disks.inventory_evidence,{full:true,hint:'Bei Auswahl per Seriennummer oder WWN erforderlich; bei automatischer Auswahl optional.'})
|
||
+ `<div class="form-grid full" data-installation-zfs-fields>${zfsFields}</div>`
|
||
+ `<div class="form-grid full" data-installation-lvm-fields>${lvmNumbers.map(([key,label,min]) => number(`disk.lvm.${key}`,label,lvm[key],min,1000000)).join('')}</div>`;
|
||
return `<div class="full installation-editor" data-installation-editor="${esc(prefix)}" data-installation-state="${esc(JSON.stringify({values,override}))}">
|
||
${override?'<p class="small-text muted">Nicht aktivierte Bereiche und leere Felder übernehmen die veröffentlichten Profilwerte. Gesperrte Profilfelder können nicht überschrieben werden.</p>':''}
|
||
<div class="form-grid">${section('global','Allgemeine Einstellungen',general)}
|
||
${section('root_secret_id','Root-Zugang',select('root_secret_id','Gespeichertes Root-Geheimnis',secretId,secrets,{required,full:true,hint:'Root-Zugang vorher unter Einstellungen → Geheimnisse anlegen.'}))}
|
||
${section('network','Managementnetz',networking)}${section('disk_setup','Systemdatenträger',storage)}</div></div>`;
|
||
}
|
||
function read(container, prefix = 'installation', previousValues = {}) {
|
||
const editor = root(container,prefix), state = JSON.parse(editor.dataset.installationState);
|
||
const values = clone(object(previousValues,'Installationskonfiguration'));
|
||
const before = state.values;
|
||
const control = name => [...editor.querySelectorAll('[name]')].find(node => node.name === `${prefix}.${name}`);
|
||
const value = name => (control(name)?.value || '').trim();
|
||
const enabled = name => !state.override || Boolean(control(`enable.${name}`)?.checked);
|
||
const assign = (target,key,next) => {if (next === undefined) delete target[key]; else target[key] = next;};
|
||
const string = name => value(name) || undefined;
|
||
const numericValue = (name,label,min,max,integer=false) => {
|
||
const node = control(name), raw = value(name);
|
||
if (node?.validity?.badInput) throw new Error(`${label}: Bitte eine gültige Zahl eingeben.`);
|
||
if (!raw) return undefined;
|
||
const parsed = Number(raw);
|
||
if (!Number.isFinite(parsed) || parsed < min || parsed > max || (integer && !Number.isInteger(parsed))) throw new Error(`${label}: ${integer?'Eine ganze Zahl':'Eine Zahl'} zwischen ${min} und ${max} eingeben.`);
|
||
if (name.startsWith('disk.lvm.') && parsed >= 1000000) throw new Error(`${label}: Die Größe muss kleiner als 1000000 GiB sein.`);
|
||
return parsed;
|
||
};
|
||
const list = (name,previous) => {
|
||
const result = readDataEditor(editor,`${prefix}.${name}`);
|
||
if (!Array.isArray(result) || result.some(item => typeof item !== 'string' || !item.trim())) throw new Error(`${name.includes('ssh')?'SSH-Schlüssel':'Datenträgerkennungen'}: Jeden Eintrag ausfüllen oder den leeren Eintrag entfernen.`);
|
||
return result.length || Array.isArray(previous) && previous.length === 0 ? result : undefined;
|
||
};
|
||
const base = name => clone(object(Object.hasOwn(values,name)?values[name]:before[name],name));
|
||
if (enabled('global')) {
|
||
const global = base('global');
|
||
for (const key of ['keyboard','country','timezone','mailto','fqdn']) assign(global,key,string(`global.${key}`));
|
||
const reboot = value('global.reboot-on-error');
|
||
if (reboot && !['true','false'].includes(reboot)) throw new Error('Neustart bei Fehler: Bitte Ja, Nein oder Übernehmen wählen.');
|
||
assign(global,'reboot-on-error',reboot ? reboot === 'true' : undefined);
|
||
assign(global,'root-ssh-keys',list('global.root-ssh-keys',before.global?.['root-ssh-keys']));
|
||
values.global = global;
|
||
} else delete values.global;
|
||
if (enabled('root_secret_id')) assign(values,'root_secret_id',string('root_secret_id'));
|
||
else delete values.root_secret_id;
|
||
if (enabled('network')) {
|
||
const network = base('network');
|
||
for (const key of ['source','gateway','dns','cidr']) assign(network,key,string(`network.${key}`));
|
||
const filter = Object.create(null);
|
||
for (const row of editor.querySelectorAll('[data-installation-network-row]')) {
|
||
const input = suffix => [...row.querySelectorAll('[name]')].find(node => node.name === `${prefix}.${suffix}`)?.value.trim() || '';
|
||
const selected = input('interface-key'), key = selected === '__custom' ? input('interface-custom') : selected === '__mac' ? 'ID_NET_NAME_MAC' : selected;
|
||
let expected = input('interface-value');
|
||
if (!expected && selected !== '__custom') continue;
|
||
if (!key || !expected) throw new Error('Management-Schnittstelle: Merkmal und erwarteten Wert vollständig ausfüllen.');
|
||
if (selected === '__mac') {
|
||
const mac = expected.replace(/[:-]/g,'').toLowerCase();
|
||
if (!/^[0-9a-f]{12}$/.test(mac)) throw new Error('Management-Schnittstelle: Eine gültige MAC-Adresse wie 02:00:00:00:00:01 eingeben.');
|
||
expected = `enx${mac}`;
|
||
}
|
||
if (['__proto__','prototype','constructor'].includes(key) || Object.hasOwn(filter,key)) throw new Error('Management-Schnittstelle: Jedes gültige Merkmal darf nur einmal vorkommen.');
|
||
filter[key] = expected;
|
||
}
|
||
const keepEmptyFilter = before.network?.filter && Object.keys(before.network.filter).length === 0;
|
||
if (!state.override && !Object.keys(filter).length) throw new Error('Bitte die MAC-Adresse oder ein anderes Merkmal der Management-Schnittstelle angeben.');
|
||
assign(network,'filter',Object.keys(filter).length || keepEmptyFilter ? {...filter} : undefined);
|
||
values.network = network;
|
||
} else delete values.network;
|
||
if (enabled('disk_setup')) {
|
||
const disks = base('disk_setup'), filesystem = value('disk.filesystem'), mode = value('disk.mode');
|
||
if (filesystem && !['zfs','ext4','xfs'].includes(filesystem)) throw new Error('Bitte ZFS, ext4 oder XFS auswählen.');
|
||
if (mode && !['all','filtered'].includes(mode)) throw new Error('Bitte eine gültige Datenträgerauswahl treffen.');
|
||
assign(disks,'filesystem',filesystem || undefined);
|
||
assign(disks,'inventory_evidence',string('disk.inventory_evidence'));
|
||
if (mode === 'all') {
|
||
const raid = value('disk.zfs.raid');
|
||
if ((filesystem && filesystem !== 'zfs') || (raid && raid !== 'raid0') || (!state.override && (!filesystem || !raid))) throw new Error('Die automatische Datenträgerauswahl benötigt ZFS mit RAID0.');
|
||
disks.selection = 'all';
|
||
for (const key of ['filter','filter_match','expected_count','expected_serials']) delete disks[key];
|
||
} else {
|
||
delete disks.selection;
|
||
const key = value('disk.filter.key'), pattern = value('disk.filter.value');
|
||
if ((key && pattern) && !['ID_SERIAL','ID_SERIAL_SHORT','ID_WWN'].includes(key)) throw new Error('Bitte Seriennummer oder WWN zur Datenträgerauswahl verwenden.');
|
||
if (mode === 'filtered' && !state.override && (!key || !pattern)) throw new Error('Bitte ein Datenträgermerkmal und einen Seriennummern- oder WWN-Filter eingeben.');
|
||
assign(disks,'filter',key && pattern ? {[key]:pattern} : undefined);
|
||
assign(disks,'filter_match',string('disk.filter_match'));
|
||
const serials = list('disk.expected_serials',before.disk_setup?.expected_serials);
|
||
if (mode === 'filtered' && !state.override && !serials?.length) throw new Error('Bitte mindestens eine bestätigte Datenträgerkennung hinzufügen.');
|
||
if (serials && (serials.length > 16 || new Set(serials).size !== serials.length || serials.some(item => /[*?\[\]]/.test(item)))) throw new Error('Datenträgerkennungen: Höchstens 16 unterschiedliche, konkrete Seriennummern oder WWNs eingeben.');
|
||
const count = numericValue('disk.expected_count','Anzahl bestätigter Datenträger',1,16,true);
|
||
if (count !== undefined && serials && count !== serials.length) throw new Error('Die Anzahl muss zur Liste der bestätigten Datenträger passen.');
|
||
assign(disks,'expected_serials',serials);
|
||
assign(disks,'expected_count',count ?? (!state.override && serials?.length ? serials.length : undefined));
|
||
if (disks.filter_match && !['all','any'].includes(disks.filter_match)) throw new Error('Bitte eine gültige Filterverknüpfung wählen.');
|
||
if (disks.inventory_evidence && disks.inventory_evidence.length < 5) throw new Error('Die Referenz zur Hardwareprüfung muss mindestens fünf Zeichen enthalten.');
|
||
}
|
||
if (filesystem === 'zfs' || !filesystem && (before.disk_setup?.zfs || [...editor.querySelectorAll('[name]')].some(node => node.name.startsWith(`${prefix}.disk.zfs.`) && node.value))) {
|
||
const zfs = clone(object(disks.zfs,'ZFS'));
|
||
assign(zfs,'raid',string('disk.zfs.raid'));
|
||
for (const [key,label,min,max,integer] of zfsNumbers) assign(zfs,key,numericValue(`disk.zfs.${key}`,label,min,max,integer));
|
||
for (const key of ['checksum','compress']) assign(zfs,key,string(`disk.zfs.${key}`));
|
||
if (zfs.raid && !raids.some(([raid]) => raid === zfs.raid)) throw new Error('Bitte einen unterstützten ZFS-Verbund auswählen.');
|
||
if (zfs.checksum && !['on','fletcher4','sha256'].includes(zfs.checksum)) throw new Error('Bitte eine unterstützte ZFS-Prüfsumme auswählen.');
|
||
if (zfs.compress && !['on','off','lzjb','lz4','zle','gzip','zstd'].includes(zfs.compress)) throw new Error('Bitte eine unterstützte ZFS-Kompression auswählen.');
|
||
const count = disks.expected_serials?.length, minimum = {raid0:1,raid1:2,raid10:4,'raidz-1':3,'raidz-2':4,'raidz-3':5};
|
||
if (mode === 'filtered' && count && zfs.raid && (count < minimum[zfs.raid] || zfs.raid === 'raid10' && count % 2)) throw new Error('Die bestätigte Datenträgeranzahl passt nicht zum gewählten ZFS-Verbund.');
|
||
disks.zfs = zfs;
|
||
if (filesystem) delete disks.lvm;
|
||
} else if (filesystem) delete disks.zfs;
|
||
if (['ext4','xfs'].includes(filesystem) || !filesystem && (before.disk_setup?.lvm || [...editor.querySelectorAll('[name]')].some(node => node.name.startsWith(`${prefix}.disk.lvm.`) && node.value))) {
|
||
if (disks.expected_serials?.length > 1) throw new Error('ext4 und XFS benötigen genau einen bestätigten Systemdatenträger.');
|
||
const lvm = clone(object(disks.lvm,'LVM'));
|
||
for (const [key,label,min] of lvmNumbers) assign(lvm,key,numericValue(`disk.lvm.${key}`,label,min,1000000));
|
||
if (Object.keys(lvm).length || Object.hasOwn(before.disk_setup || {},'lvm')) disks.lvm = lvm;
|
||
else delete disks.lvm;
|
||
} else if (filesystem) delete disks.lvm;
|
||
values.disk_setup = disks;
|
||
} else delete values.disk_setup;
|
||
if (state.override) for (const key of ['global','network','disk_setup']) {
|
||
if (values[key] && !Object.keys(values[key]).length && (!Object.hasOwn(before,key) || Object.keys(before[key]).length)) delete values[key];
|
||
}
|
||
return values;
|
||
}
|
||
function wire(container, prefix = 'installation') {
|
||
const editor = root(container,prefix), state = JSON.parse(editor.dataset.installationState);
|
||
if (editor.dataset.installationWired) return;
|
||
editor.dataset.installationWired = 'true';
|
||
wireDataEditors(editor);
|
||
const control = name => [...editor.querySelectorAll('[name]')].find(node => node.name === `${prefix}.${name}`);
|
||
const visible = (node,show) => {node.hidden = !show; for (const item of node.querySelectorAll('input,select,textarea,button')) item.disabled = !show;};
|
||
function refresh() {
|
||
for (const section of editor.querySelectorAll('[data-installation-section]')) {
|
||
const enabled = !state.override || control(`enable.${section.dataset.installationSection}`).checked;
|
||
visible(section.querySelector('[data-installation-section-body]'),enabled);
|
||
}
|
||
const diskEnabled = !state.override || control('enable.disk_setup').checked;
|
||
const filesystem = control('disk.filesystem').value, mode = control('disk.mode').value;
|
||
visible(editor.querySelector('[data-installation-filter-fields]'),diskEnabled && mode !== 'all');
|
||
visible(editor.querySelector('[data-installation-zfs-fields]'),diskEnabled && (!filesystem || filesystem === 'zfs'));
|
||
visible(editor.querySelector('[data-installation-lvm-fields]'),diskEnabled && (!filesystem || ['ext4','xfs'].includes(filesystem)));
|
||
editor.querySelector('[data-installation-all-note]').hidden = !diskEnabled || mode !== 'all';
|
||
control('disk.inventory_evidence').required = !state.override && mode === 'filtered';
|
||
for (const row of editor.querySelectorAll('[data-installation-network-row]')) {
|
||
const select = [...row.querySelectorAll('[name]')].find(node => node.name === `${prefix}.interface-key`);
|
||
const expected = [...row.querySelectorAll('[name]')].find(node => node.name === `${prefix}.interface-value`);
|
||
expected.placeholder = select.value === '__mac' ? '02:00:00:00:00:01' : 'Erwarteter Merkmalswert';
|
||
visible(row.querySelector('[data-installation-custom-key]'),(!state.override || control('enable.network').checked) && select.value === '__custom');
|
||
}
|
||
}
|
||
editor.addEventListener('change', event => {
|
||
const name = event.target.name;
|
||
if (name === `${prefix}.disk.mode` && event.target.value === 'all') {
|
||
control('disk.filesystem').value = 'zfs';
|
||
control('disk.zfs.raid').value = 'raid0';
|
||
}
|
||
if (name === `${prefix}.disk.filesystem` && event.target.value && event.target.value !== 'zfs' && control('disk.mode').value === 'all') control('disk.mode').value = 'filtered';
|
||
if (name === `${prefix}.disk.zfs.raid` && event.target.value !== 'raid0' && control('disk.mode').value === 'all') control('disk.mode').value = 'filtered';
|
||
refresh();
|
||
});
|
||
editor.addEventListener('click', event => {
|
||
const add = event.target.closest('[data-installation-add-interface]'), remove = event.target.closest('[data-installation-remove-interface]');
|
||
if (add) editor.querySelector('[data-installation-network-rows]').insertAdjacentHTML('beforeend',networkRow(prefix));
|
||
if (remove) remove.closest('[data-installation-network-row]').remove();
|
||
if (add || remove) {event.preventDefault(); refresh();}
|
||
});
|
||
refresh();
|
||
}
|
||
return {render,read,wire};
|
||
})();
|
||
|
||
function installationEditor(values = {}, options = {}) { return installationForm.render(values,options); }
|
||
function readInstallationEditor(container,prefix = 'installation',previousValues = {}) { return installationForm.read(container,prefix,previousValues); }
|
||
function wireInstallationEditor(container,prefix = 'installation') { installationForm.wire(container,prefix); }
|