154 lines
4.9 KiB
TypeScript
154 lines
4.9 KiB
TypeScript
|
|
/**
|
||
|
|
* ISO Builder Orchestrator
|
||
|
|
* Coordinates all ISO building operations
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { log } from '../logging.ts';
|
||
|
|
import { path } from '../plugins.ts';
|
||
|
|
import { ensureDir } from '../paths.ts';
|
||
|
|
import type { IIsoConfig } from '../interfaces/iso-config.interface.ts';
|
||
|
|
|
||
|
|
import { IsoCache } from './iso-cache.ts';
|
||
|
|
import { IsoDownloader } from './iso-downloader.ts';
|
||
|
|
import { IsoExtractor } from './iso-extractor.ts';
|
||
|
|
import { IsoPacker } from './iso-packer.ts';
|
||
|
|
import { CloudInitGenerator } from './cloud-init-generator.ts';
|
||
|
|
|
||
|
|
export class IsoBuilder {
|
||
|
|
private cache: IsoCache;
|
||
|
|
private extractor: IsoExtractor;
|
||
|
|
private packer: IsoPacker;
|
||
|
|
private cloudInitGen: CloudInitGenerator;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.cache = new IsoCache();
|
||
|
|
this.extractor = new IsoExtractor();
|
||
|
|
this.packer = new IsoPacker();
|
||
|
|
this.cloudInitGen = new CloudInitGenerator();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build a customized ISO from configuration
|
||
|
|
*/
|
||
|
|
async build(config: IIsoConfig, onProgress?: (step: string, progress: number) => void): Promise<void> {
|
||
|
|
log.info('Starting ISO build process...');
|
||
|
|
|
||
|
|
// Step 1: Check dependencies
|
||
|
|
onProgress?.('Checking dependencies', 10);
|
||
|
|
await this.extractor.ensureDependencies();
|
||
|
|
await this.packer.ensureDependencies();
|
||
|
|
|
||
|
|
// Step 2: Get or download base ISO
|
||
|
|
onProgress?.('Obtaining base ISO', 20);
|
||
|
|
const baseIsoPath = await this.getBaseIso(config.iso.ubuntu_version, config.iso.architecture);
|
||
|
|
|
||
|
|
// Step 3: Extract ISO
|
||
|
|
onProgress?.('Extracting ISO', 40);
|
||
|
|
const extractedDir = await this.extractor.extract(baseIsoPath);
|
||
|
|
|
||
|
|
// Step 4: Generate cloud-init configuration
|
||
|
|
onProgress?.('Generating cloud-init configuration', 60);
|
||
|
|
const cloudInitDir = path.join(extractedDir, 'nocloud');
|
||
|
|
await ensureDir(cloudInitDir);
|
||
|
|
|
||
|
|
if (config.cloud_init) {
|
||
|
|
const networkConfig = config.network ? {
|
||
|
|
wifi: config.network.wifi,
|
||
|
|
} : undefined;
|
||
|
|
|
||
|
|
await this.cloudInitGen.writeCloudInitFiles(
|
||
|
|
cloudInitDir,
|
||
|
|
config.cloud_init,
|
||
|
|
networkConfig,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 5: Inject custom boot scripts (if any)
|
||
|
|
if (config.boot_scripts && config.boot_scripts.length > 0) {
|
||
|
|
onProgress?.('Injecting boot scripts', 70);
|
||
|
|
await this.injectBootScripts(extractedDir, config.boot_scripts);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Step 6: Update grub configuration to use cloud-init
|
||
|
|
onProgress?.('Updating boot configuration', 80);
|
||
|
|
await this.updateBootConfig(extractedDir);
|
||
|
|
|
||
|
|
// Step 7: Repack ISO
|
||
|
|
onProgress?.('Creating customized ISO', 90);
|
||
|
|
const outputPath = path.join(config.output.path, config.output.filename);
|
||
|
|
await ensureDir(config.output.path);
|
||
|
|
await this.packer.pack(extractedDir, outputPath, 'UBUNTU_CUSTOM');
|
||
|
|
|
||
|
|
// Step 8: Cleanup
|
||
|
|
onProgress?.('Cleaning up', 95);
|
||
|
|
await Deno.remove(extractedDir, { recursive: true });
|
||
|
|
|
||
|
|
onProgress?.('Complete', 100);
|
||
|
|
log.success(`ISO built successfully: ${outputPath}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get base ISO (from cache or download)
|
||
|
|
*/
|
||
|
|
private async getBaseIso(version: string, arch: 'amd64' | 'arm64'): Promise<string> {
|
||
|
|
// Check cache first
|
||
|
|
const cachedPath = await this.cache.getPath(version, arch);
|
||
|
|
if (cachedPath) {
|
||
|
|
log.info(`Using cached ISO: ${cachedPath}`);
|
||
|
|
return cachedPath;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Download to cache
|
||
|
|
log.info(`Downloading Ubuntu ${version} ${arch}...`);
|
||
|
|
return await this.cache.downloadAndCache(version, arch, (downloaded, total) => {
|
||
|
|
const percent = ((downloaded / total) * 100).toFixed(1);
|
||
|
|
process.stdout.write(`\r📥 Download progress: ${percent}%`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Inject custom boot scripts
|
||
|
|
*/
|
||
|
|
private async injectBootScripts(
|
||
|
|
extractedDir: string,
|
||
|
|
bootScripts: Array<{ name: string; path: string; enable?: boolean }>,
|
||
|
|
): Promise<void> {
|
||
|
|
for (const script of bootScripts) {
|
||
|
|
log.info(`Injecting boot script: ${script.name}`);
|
||
|
|
|
||
|
|
// Copy script to ISO
|
||
|
|
const destPath = path.join(extractedDir, 'scripts', script.name);
|
||
|
|
await ensureDir(path.dirname(destPath));
|
||
|
|
await Deno.copyFile(script.path, destPath);
|
||
|
|
|
||
|
|
// Make executable
|
||
|
|
await Deno.chmod(destPath, 0o755);
|
||
|
|
|
||
|
|
// TODO: Create systemd service if enable is true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update boot configuration to enable cloud-init with nocloud datasource
|
||
|
|
*/
|
||
|
|
private async updateBootConfig(extractedDir: string): Promise<void> {
|
||
|
|
// Update grub config to add cloud-init nocloud datasource
|
||
|
|
const grubCfgPath = path.join(extractedDir, 'boot', 'grub', 'grub.cfg');
|
||
|
|
|
||
|
|
try {
|
||
|
|
let grubContent = await Deno.readTextFile(grubCfgPath);
|
||
|
|
|
||
|
|
// Add cloud-init datasource parameter
|
||
|
|
grubContent = grubContent.replace(
|
||
|
|
/linux\s+\/casper\/vmlinuz/g,
|
||
|
|
'linux /casper/vmlinuz ds=nocloud;s=/cdrom/nocloud/',
|
||
|
|
);
|
||
|
|
|
||
|
|
await Deno.writeTextFile(grubCfgPath, grubContent);
|
||
|
|
log.success('Updated boot configuration for cloud-init');
|
||
|
|
} catch (err) {
|
||
|
|
log.warn(`Could not update grub config: ${err instanceof Error ? err.message : String(err)}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|