UNPKG

penguins-eggs

Version:

A remaster system tool, compatible with Almalinux, Alpine, Arch, Debian, Devuan, Fedora, Manjaro, Opensuse, Ubuntu and derivatives

365 lines (364 loc) 15.7 kB
/** * ./src/classes/pxe.ts * penguins-eggs v.25.7.x / ecmascript 2020 * author: Piero Proietti * email: piero.proietti@gmail.com * license: MIT */ import express from 'express'; import fs from 'node:fs'; import path from 'node:path'; // @ts-ignore import tftp from 'tftp'; import { startSimpleProxy } from '../dhcpd-proxy/simple-proxy.js'; import { exec } from '../lib/utils.js'; import Distro from './distro.js'; import Diversions from './diversions.js'; import Settings from './settings.js'; import Utils from './utils.js'; // _dirname const __dirname = path.dirname(new URL(import.meta.url).pathname); /** * Pxe: */ export default class Pxe { bootLabel = ''; distro = {}; echo = {}; eggRoot = ''; initrdImg = ''; isos = []; // cuckoo's eggs nest = ''; pxeRoot = ''; settings = {}; vmlinuz = ''; /** * constructor * @param nest * @param pxeRoot */ constructor(nest = '', pxeRoot = '', verbose = false) { this.nest = nest; this.pxeRoot = pxeRoot; this.echo = Utils.setEcho(verbose); } /** * build */ async build() { const echoYes = Utils.setEcho(true); // pxeRoot erase if (fs.existsSync(this.pxeRoot)) { await exec(`rm ${this.pxeRoot} -rf`, this.echo); } // Struttura await exec(`mkdir ${this.pxeRoot} -p`, this.echo); await exec(`ln -s ${path.join(this.eggRoot, 'live')} ${path.join(this.pxeRoot, 'live')}`, this.echo); await exec(`ln -s ${this.nest}.disk ${path.join(this.pxeRoot, '.disk')}`, this.echo); // Link supplementari distro if (this.distro.familyId === 'archlinux') { let filesystemName = `arch/x86_64/airootfs.sfs`; if (Diversions.isManjaroBased(this.settings.distro.distroId)) { filesystemName = `manjaro/x86_64/livefs.sfs`; } await exec(`mkdir ${path.join(this.pxeRoot, path.dirname(filesystemName))} -p`, this.echo); await exec(`ln -s ${path.join(this.eggRoot, 'live/filesystem.squashfs')} ${path.join(this.pxeRoot, filesystemName)}`, this.echo); } // Firewall per fedora if (this.distro.familyId === 'fedora' || this.distro.familyId === 'opensuse') { await exec(`firewall-cmd --add-service=dhcp --permanent`, this.echo); await exec(`firewall-cmd --add-service=tftp --permanent`, this.echo); await exec(`firewall-cmd --add-service=http --permanent`, this.echo); await exec(`firewall-cmd --reload`, this.echo); } await this.grubCfg(); await this.bios(); await this.uefi(); await this.http(); } /** * * @param dhcpOptions */ dhcpdStart(dhcpOptions) { startSimpleProxy(dhcpOptions); } /** * fertilization() * * cuckoo's nest */ async fertilization() { this.settings = new Settings(); this.settings.load(); this.distro = new Distro(); if (Utils.isLive()) { this.eggRoot = this.distro.liveMediumPath; } else { this.eggRoot = path.join(this.settings.config.snapshot_dir, 'mnt/iso'); } if (!Utils.isLive() && !fs.existsSync(this.settings.config.snapshot_dir)) { console.log('no image available, build an image with: sudo eggs produce'); process.exit(); } /** * se pxeRoot non esiste viene creato */ if (!fs.existsSync(this.pxeRoot)) { await exec(`mkdir ${this.pxeRoot} -p`); } /** * Ricerca delle ISOs * installed: /home/eggs/mnt/iso/live * live: this.iso/live */ const isos = []; const pathFiles = path.join(this.eggRoot, 'live'); const files = fs.readdirSync(pathFiles); // rieva nome vmlinuz e initrdImg for (const file of files) { if (path.basename(file).slice(0, 7) === 'vmlinuz') { this.vmlinuz = path.basename(file); } if (path.basename(file).slice(0, 4) === 'init') { this.initrdImg = path.basename(file); } } /** * bootLabel */ this.bootLabel = 'not found'; if (fs.existsSync(path.join(this.eggRoot, '.disk/mkisofs'))) { const a = fs.readFileSync(path.join(this.eggRoot, '.disk/mkisofs'), 'utf8'); const b = a.slice(Math.max(0, a.indexOf('-o ') + 3)); const c = b.slice(0, Math.max(0, b.indexOf(' '))); this.bootLabel = c.slice(Math.max(0, c.lastIndexOf('/') + 1)); } console.log(`eggRoot: ${this.eggRoot}`); console.log(`bootLabel: ${this.bootLabel}`); console.log(`vmlinuz: ${this.vmlinuz}`); console.log(`initrd: ${this.initrdImg}`); } /** * start http server for images */ async httpStart() { const port = 80; const httpRoot = this.pxeRoot; // 1. Crea un'applicazione Express const app = express(); // 2. Usa il middleware di Express per servire i file statici. app.use(express.static(httpRoot)); // 3. Avvia il server app.listen(port, () => { console.log(`HTTP server (Express) listening on 0.0.0.0:${port}`); console.log(`Serving files from ${httpRoot}`); }); } /** * start tftp */ async tftpStart(tftpOptions) { const tftpServer = tftp.createServer(tftpOptions); console.log('tftp listening: ' + tftpOptions.host + ':' + tftpOptions.port); tftpServer.on('error', (error) => { // Errors from the main socket // The current transfers are not aborted console.error(error); }); tftpServer.on('request', (req, res) => { req.on('error', (error) => { // Error from the request // The connection is already closed console.error('[' + req.stats.remoteAddress + ':' + req.stats.remotePort + '] (' + req.file + ') ' + error.message); }); }); tftpServer.listen(); } /** * Metodo helper per ottenere i parametri corretti per GRUB * in base alla famiglia della distribuzione. */ _getKernelParameters() { let lp = ''; // .replaceAll(/\s\s+/g, ' ') switch (this.distro.familyId) { case 'alpine': { lp = `alpine_repo=http://${Utils.address()}/live/filesystem.squashfs \ modules=loop,squashfs,sd-mod,usb-storage,virtio-net,e1000e \ acpi=off \ ip=dhcp`; break; } case 'archlinux': { let basedir = 'archisobasedir=arch'; let hook = 'archiso_http_srv'; if (Diversions.isManjaroBased(this.distro.distroId)) { basedir = ''; hook = 'miso_http_srv'; } lp = `${hook}=http://${Utils.address()}/ \ ${basedir} \ ip=dhcp \ copytoram=n`; break; } case 'debian': { lp = `fetch=http://${Utils.address()}/live/filesystem.squashfs \ boot=live \ config \ noswap \ noprompt \ ip=dhcp`; break; } case 'fedora': case 'openmamba': case 'opensuse': { lp = `initrd=http://${Utils.address()}/live/${path.basename(this.initrdImg)} \ root=live:http://${Utils.address()}/live/filesystem.squashfs \ rootfstype=auto \ ro \ rd.live.image \ rd.luks=0 \ rd.md=0 \ rd.dm=0\n`; break; } default: { console.warn(`Attenzione: famiglia distro '${this.distro.familyId}' non riconosciuta per GRUB.`); } } return lp.replaceAll(/\s\s+/g, ' '); } /** * configure PXE bios */ async bios() { const bootloaders = Diversions.bootloaders(this.distro.familyId); await exec(`cp ${path.join(__dirname, '../../addons/eggs/theme/livecd/isolinux.theme.cfg')} ${path.join(this.pxeRoot, 'isolinux.theme.cfg')}`, this.echo); await exec(`cp ${path.join(__dirname, '../../addons/eggs/theme/livecd/splash.png')} ${path.join(this.pxeRoot, 'splash.png')}`, this.echo); // ipxe.pxe await exec(`ln -s ${path.join(bootloaders, 'ipxe/ipxe.pxe')} ${path.join(this.pxeRoot, 'ipxe.pxe')}`, this.echo); // snponly.efi await exec(`ln -s ${path.join(bootloaders, 'ipxe/snponly.efi')} ${path.join(this.pxeRoot, 'snponly.efi')}`, this.echo); // pxe await exec(`cp ${path.join(bootloaders, 'PXELINUX/pxelinux.0')} ${path.join(this.pxeRoot, 'pxelinux.0')}`, this.echo); await exec(`cp ${path.join(bootloaders, 'PXELINUX/lpxelinux.0')} ${path.join(this.pxeRoot, 'lpxelinux.0')}`, this.echo); // syslinux await exec(`ln -s ${path.join(bootloaders, 'syslinux/modules/bios/ldlinux.c32')} ${path.join(this.pxeRoot, 'ldlinux.c32')}`, this.echo); await exec(`ln -s ${path.join(bootloaders, 'syslinux/modules/bios/vesamenu.c32')} ${path.join(this.pxeRoot, 'vesamenu.c32')}`, this.echo); await exec(`ln -s ${path.join(bootloaders, 'syslinux/modules/bios/libcom32.c32')} ${path.join(this.pxeRoot, 'libcom32.c32')}`, this.echo); await exec(`ln -s ${path.join(bootloaders, 'syslinux/modules/bios/libutil.c32')} ${path.join(this.pxeRoot, 'libutil.c32')}`, this.echo); await exec(`ln -s ${path.join(bootloaders, 'syslinux/memdisk')} ${path.join(this.pxeRoot, 'memdisk')}`, this.echo); await exec(`mkdir ${path.join(this.pxeRoot, 'pxelinux.cfg')}`, this.echo); let content = ''; content += '# eggs: pxelinux.cfg/default\n'; content += '# search path for the c32 support libraries (libcom32, libutil etc.)\n'; content += `path /\n`; content += 'include isolinux.theme.cfg\n'; content += 'UI vesamenu.c32\n'; content += '\n'; content += `menu title cuckoo: when you need a flying PXE server! ${Utils.address()}\n`; content += 'PROMPT 0\n'; content += 'TIMEOUT 200\n'; content += '\n'; content += `label ${this.distro.distroId}\n`; content += `menu label ${this.bootLabel.replace('.iso', '')}\n`; content += `kernel http://${Utils.address()}/live/${path.basename(this.vmlinuz)}\n`; const kernelParams = this._getKernelParameters(); content += `append initrd=http://${Utils.address()}/live/${path.basename(this.initrdImg)} ${kernelParams}\n`; const file = path.join(this.pxeRoot, 'pxelinux.cfg/default'); fs.writeFileSync(file, content); } /** * grubCfg * @param familyId */ async grubCfg() { const bootloaders = Diversions.bootloaders(this.distro.familyId); const echoYes = Utils.setEcho(true); /** * On Debian bookworm: */ await exec(`mkdir -p ${path.join(this.pxeRoot, 'grub')}`, this.echo); if (this.distro.familyId === 'debian') { switch (process.arch) { case 'arm64': { await exec(`cp ${path.join(bootloaders, '/grub/arm64-efi-signed/grubnetaa64.efi.signed')} ${path.join(this.pxeRoot, 'grub.efi')}`, this.echo); await exec(`cp -r ${path.join(bootloaders, '/grub/arm64-efi')} ${path.join(this.pxeRoot, 'grub')}`, this.echo); break; } case 'ia32': { await exec(`cp ${path.join(bootloaders, '/grub/i386-efi-signed/grubnetia32.efi.signed')} ${path.join(this.pxeRoot, 'grub.efi')}`, this.echo); await exec(`cp -r ${path.join(bootloaders, '/grub/i386-efi')} ${path.join(this.pxeRoot, 'grub')}`, this.echo); break; } case 'x64': { await exec(`cp ${path.join(bootloaders, '/grub/x86_64-efi-signed/grubnetx64.efi.signed')} ${path.join(this.pxeRoot, 'grub.efi')}`, this.echo); await exec(`cp -r ${path.join(bootloaders, '/grub/x86_64-efi')} ${path.join(this.pxeRoot, 'grub')}`, this.echo); break; } // No default } } else { /** * le altre distribuzione not signed */ await exec(`cp ${path.join(bootloaders, 'grub/x86_64-efi/monolithic/grubnetx64.efi')} ${path.join(this.pxeRoot, 'grub.efi')}`, this.echo); await exec(`cp -r ${path.join(bootloaders, '/grub/x86_64-efi')} ${path.join(this.pxeRoot, 'grub')}`, this.echo); } // Genera il file grub.cfg const grubName = path.join(this.pxeRoot, 'grub/grub.cfg'); // Il file deve chiamarsi grub.cfg let grubContent = ''; grubContent += `set timeout=10\n`; grubContent += `set default=0\n\n`; // Titolo del menu dinamico grubContent += `menuentry "${this.bootLabel.replace('.iso', '')} via PXE" {\n`; grubContent += ` echo "Booting ${this.bootLabel.replace('.iso', '')}..."\n`; grubContent += ` echo "Loading Linux Kernel..."\n`; const kernelParams = this._getKernelParameters(); grubContent += ` linux (http,${Utils.address()})/live/${path.basename(this.vmlinuz)} ${kernelParams}\n`; grubContent += ` echo "Loading Initial Ramdisk..."\n`; grubContent += ` initrd (http,${Utils.address()})/live/${path.basename(this.initrdImg)}\n`; grubContent += `}\n`; fs.writeFileSync(grubName, grubContent, 'utf-8'); } /** * configure PXE http server */ async http() { const file = path.join(this.pxeRoot, 'index.html'); let content = ''; content += "<html><title>Penguin's eggs PXE server</title>"; content += '<div style="background-image:url(\'/splash.png\');background-repeat:no-repeat;width: 640;height:480;padding:5px;border:1px solid black;">'; content += '<h1>Cuckoo PXE server</h1>'; content += `<body>address: <a href=http://${Utils.address()}>${Utils.address()}</a><br/>`; if (Utils.isLive()) { content += 'started from live iso image<br/>'; } else { content += 'Serving:<li>'; for (const iso of this.isos) { content += `<ul><a href='http://${Utils.address()}/${iso}'>${iso}</a></ul>`; } content += '</li>'; } content += "source: <a href='https://github.com/pieroproietti/penguins-eggs'>https://github.com/pieroproietti/penguins-eggs</a><br/>"; content += "manual: <a href='https://penguins-eggs.net/book/italiano9.2.html'>italiano</a>, <a href='https://penguins--eggs-net.translate.goog/book/italiano9.2?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en'>translated</a><br/>"; content += "discuss: <a href='https://t.me/penguins_eggs'>Telegram group<br/></body</html>"; fs.writeFileSync(file, content); } /** * uefi: uso ipxe solo per chainload di grub */ async uefi() { let content = '#!ipxe\n'; content += 'dhcp\n'; content += `chain http://${Utils.address()}/grub.efi\n`; const file = path.join(this.pxeRoot, 'autoexec.ipxe'); fs.writeFileSync(file, content); } }