feat(core): Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts
This commit is contained in:
153
ts/classes/iso-builder.ts
Normal file
153
ts/classes/iso-builder.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user