UNPKG

etcher-sdk

Version:
313 lines • 16.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.migrate = void 0; const process = require("process"); const check_disk_space_1 = require("check-disk-space"); const diskpart = require("../diskpart"); const source_destination_1 = require("../source-destination"); const helpers_1 = require("./helpers"); const copy_bootloader_1 = require("./copy-bootloader"); const windows_commands_1 = require("./windows-commands"); const util_1 = require("util"); const child_process_1 = require("child_process"); const execAsync = (0, util_1.promisify)(child_process_1.exec); const os_1 = require("os"); const fs_1 = require("fs"); // Expected paritition labels from flasher image const BOOT_PARTITION_LABEL = 'flash-boot'; const ROOTA_PARTITION_LABEL = 'flash-rootA'; /** Determine if running as administrator. */ async function isElevated() { // `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10 // Works even when the "Server" service is disabled // See http://stackoverflow.com/a/28268802 try { await execAsync('fltmc'); } catch (error) { if (error.code === os_1.constants.errno.EPERM) { return false; } throw error; } return true; } function formatMB(bytes) { return (bytes / (1024 * 1024)).toFixed(2); } /** * Opens filesystem on boot partition so can read/write files. Assigns drive letter * to provided partition on success. * * @param {string} driveLetter - to be assigned; caller must ensure unused * @param {TargetPartition} partition - for the boot partition * @param {TargetDevice} device - device on which the partition is located * * @throws If can't find volume for boot partition */ async function openBootDrive(driveLetter, partition, device) { // Device must be online to write file. partition.volumeId = await diskpart.findVolume(device.name, BOOT_PARTITION_LABEL); if (!partition.volumeId) { throw Error(`Can't find Windows volume for boot partition`); } await diskpart.setDriveLetter(partition.volumeId, driveLetter); partition.driveLetter = driveLetter; } /** * @summary Sets up a UEFI based computer running Windows to switch to balenaOS, and then reboots to execute the switch. * !!! WARNING !!! Running this function will OVERWRITE AND DESTROY the operating system running on this computer. * * Migration copies a balenaOS boot partition and rootA partition from an image file to * the computer's storage, as well as a bootloader to trigger booting into the boot * partition. The migration is executed as a sequence of tasks as shown below, and begins * with an implicit "analyze" task that always is performed. * * After a reboot, the computer may boot to the Windows system paritition, or it may * boot to the newly copied flasher boot parition. So we must allow for failover in either case. * * The migration outputs useful status messages to the console. Migration catches * errors thrown within, outputs them to the console, and returns MigrateResult.ERROR. * * The migration may be re-run on a computer to support development or a failure in the * original run. A task may be omitted by listing it in the options.omitTasks parameter. * * * shrink -- shrink existing Windows partition as needed to accommodate the new partitions * * copy -- create/copy partitions from image, and add failover to Windows for boot partition * * config -- write host configuration like networking to boot partition * * bootloader -- copy/setup grub bootloader to EFI system partition * * reboot -- actually execute the reboot * * @param {string} imagePath - balenaOS flasher image to use as source * @param {string} windowsPartition - partition label of the device where we want to add the new data; defauls to "C" * @param {string} deviceName - storage device name, default: '\\.\PhysicalDrive0' * @param {string} efiLabel - label to use when mounting the EFI partition, in case the default "M" is already in use; * letter following efiLabel is used when mounting boot partition to write host config * @param {MigrateOptions} options - various options to qualify how migrate runs * @returns {MigrateResult} OK if no errors, FAIL on error */ const migrate = async (imagePath, windowsPartition = 'C', deviceName = '\\\\.\\PhysicalDrive0', efiLabel = 'M', options = { omitTasks: '', connectionProfiles: [] }) => { console.log(`Migrate ${deviceName} with image ${imagePath}`); try { const BOOT_PARTITION_INDEX = 1; const ROOTA_PARTITION_INDEX = 2; const BOOT_FILES_SOURCE_PATH = '/EFI/BOOT'; const BOOT_FILES_TARGET_PATH = '/EFI/Boot'; const REBOOT_DELAY_SEC = 10; const ALL_TASKS = ['shrink', 'copy', 'config', 'bootloader', 'reboot']; // initial validations if (process.platform !== 'win32') { throw Error('Platform is not Windows'); } if (!(await isElevated())) { throw Error('User is not administrator'); } if (!(0, fs_1.existsSync)(imagePath)) { throw Error(`Image ${imagePath} not found`); } if (efiLabel === 'Z') { throw Error("Can't use last letter of alphabet for EFI label"); } const tasks = ALL_TASKS.filter((task) => !options.omitTasks.includes(task)); // Define objects for image file source for partitions, storage device target, // and the target's partition table. const sourceFile = new source_destination_1.File({ path: imagePath }); const targetDevice = { name: deviceName, etcher: await (0, helpers_1.getTargetBlockDevice)(windowsPartition), }; let etcherPartitions = await targetDevice.etcher.getPartitionTable(); if (etcherPartitions === undefined) { throw Error("Can't read partition table"); } // Log existing partitions for debugging console.log('\nPartitions on target:'); for (const p of etcherPartitions.partitions) { // Satisfy TypeScript that p is not an MBRPartition even though we tested above on the table if (!('guid' in p)) { continue; } console.log(`index ${p.index}, offset ${p.offset}, type ${p.type}`); } // Prepare to check for the balenaOS boot and rootA partitions already present. // If partitions not present, determine required partition sizes and free space. // Calculations are in units of bytes. However, on Windows, required sizes are in MB. const bootPartition = { volumeId: '', driveLetter: '', }; const rootAPartition = { volumeId: '', driveLetter: '', }; // Values are rounded up to the nearest MB due to tool limitations. let requiredBootSize = 0; let requiredRootASize = 0; // Look for boot partition on a FAT16 filesystem bootPartition.etcher = await (0, helpers_1.findFilesystemLabel)(etcherPartitions, targetDevice.etcher, BOOT_PARTITION_LABEL, 'fat16'); if (bootPartition.etcher) { console.log(`Boot partition already exists at index ${bootPartition.etcher.index}`); bootPartition.volumeId = await diskpart.findVolume(targetDevice.name, BOOT_PARTITION_LABEL); console.log(`flasherBootPartition volume: ${bootPartition.volumeId}`); } else { console.log('Boot partition not found on target'); requiredBootSize = await (0, helpers_1.calcRequiredPartitionSize)(sourceFile, BOOT_PARTITION_INDEX); console.log(`Require ${requiredBootSize} (${formatMB(requiredBootSize)} MB) for boot partition`); } // Look for rootA partition on an ext4 filesystem rootAPartition.etcher = await (0, helpers_1.findFilesystemLabel)(etcherPartitions, targetDevice.etcher, ROOTA_PARTITION_LABEL, 'ext4'); if (rootAPartition.etcher) { console.log(`RootA partition already exists at index ${rootAPartition.etcher.index}`); rootAPartition.volumeId = await diskpart.findVolume(targetDevice.name, ROOTA_PARTITION_LABEL); console.log(`flasherRootAPartition volume: ${rootAPartition.volumeId}`); } else { console.log('RootA partition not found on target'); requiredRootASize = await (0, helpers_1.calcRequiredPartitionSize)(sourceFile, ROOTA_PARTITION_INDEX); console.log(`Require ${requiredRootASize} (${formatMB(requiredRootASize)} MB) for rootA partition`); } const requiredFreeSize = requiredBootSize + requiredRootASize; // Shrink Windows partition as needed to provide required unallocated space. // Shrink amount must be for *all* of required space to ensure it is contiguous. // IOW, don't assume the shrink will merge with any existing unallocated space. if (requiredFreeSize) { const unallocSpace = (await diskpart.getUnallocatedSize(targetDevice.name)) * 1024; console.log(`Found ${unallocSpace} (${formatMB(unallocSpace)} MB) not allocated on disk ${targetDevice.name}`); if (unallocSpace < requiredFreeSize) { // must force upper case const freeSpace = await (0, check_disk_space_1.default)(`${windowsPartition.toUpperCase()}:\\`); if (freeSpace.free < requiredFreeSize) { throw Error(`Need at least ${requiredFreeSize} (${formatMB(requiredFreeSize)} MB) free on partition ${windowsPartition}`); } if (tasks.includes('shrink')) { console.log(`\nShrink partition ${windowsPartition} by ${requiredFreeSize} (${formatMB(requiredFreeSize)} MB)`); await diskpart.shrinkPartition(windowsPartition, requiredFreeSize / (1024 * 1024)); } else { console.log(`\nSkip task: shrink partition ${windowsPartition} by ${requiredFreeSize} (${formatMB(requiredFreeSize)} MB)`); } } else { console.log('Unallocated space on target is sufficient for copy'); } } if (tasks.includes('copy')) { // create partitions console.log(''); // force newline if (!bootPartition.etcher) { console.log('Create flasherBootPartition'); await diskpart.createPartition(targetDevice.name, requiredBootSize / (1024 * 1024)); const afterFirstPartitions = await targetDevice.etcher.getPartitionTable(); const firstNewPartition = (0, helpers_1.findNewPartitions)(etcherPartitions, afterFirstPartitions); if (firstNewPartition.length !== 1) { throw Error(`Found ${firstNewPartition.length} new partitions for flasher boot, but expected 1`); } bootPartition.etcher = firstNewPartition[0]; console.log(`Created new partition for boot at offset ${bootPartition.etcher.offset} with size ${bootPartition.etcher.size}`); etcherPartitions = afterFirstPartitions; } if (!rootAPartition.etcher) { console.log('Create flasherRootAPartition'); await diskpart.createPartition(targetDevice.name, requiredRootASize / (1024 * 1024)); const afterSecondPartitions = await targetDevice.etcher.getPartitionTable(); const secondNewPartition = (0, helpers_1.findNewPartitions)(etcherPartitions, afterSecondPartitions); if (secondNewPartition.length !== 1) { throw Error(`Found ${secondNewPartition.length} new partitions for flasher rootA, but expected 1`); } rootAPartition.etcher = secondNewPartition[0]; console.log(`Created new partition for data at offset ${rootAPartition.etcher.offset} with size ${rootAPartition.etcher.size}`); etcherPartitions = afterSecondPartitions; } // copy partition data console.log('Copy flasherBootPartition from image to disk'); if (bootPartition.volumeId) { // Must ensure volume offline before overwrite. await diskpart.setPartitionOnlineStatus(bootPartition.volumeId, false); } try { // Sets volume online only if a new partition. await (0, helpers_1.copyPartitionFromImageToDevice)(sourceFile, 1, targetDevice.etcher, bootPartition.etcher.offset); console.log('Copy complete'); console.log('Copy flasherRootAPartition from image to disk'); // We never set rootA partition online, so no need to offline. await (0, helpers_1.copyPartitionFromImageToDevice)(sourceFile, 2, targetDevice.etcher, rootAPartition.etcher.offset); console.log('Copy complete'); } finally { if (bootPartition.volumeId) { // Ensure online if set offline above, to find volume ID from partition label. await diskpart.setPartitionOnlineStatus(bootPartition.volumeId, true); } } // Open volume so can revise grub files on boot partition to failover to Windows. if (!bootPartition.driveLetter) { const driveLetter = String.fromCharCode(efiLabel.charCodeAt(0) + 1); await openBootDrive(driveLetter, bootPartition, targetDevice); } try { await (0, copy_bootloader_1.copyBootloaderFromImage)(imagePath, 1, BOOT_FILES_SOURCE_PATH, `${bootPartition.driveLetter}:${BOOT_FILES_TARGET_PATH}`); } finally { // Just leave boot volume open if config task will write to it as well below. if (!tasks.includes('config')) { await diskpart.clearDriveLetter(bootPartition.volumeId, bootPartition.driveLetter); } } } else { console.log(`\nSkip task: create and copy partitions`); } if (tasks.includes('config')) { console.log('\nWrite network configuration'); if (!bootPartition.driveLetter) { const driveLetter = String.fromCharCode(efiLabel.charCodeAt(0) + 1); await openBootDrive(driveLetter, bootPartition, targetDevice); } try { for (const profile of options.connectionProfiles) { const pathname = `${bootPartition.driveLetter}:\\system-connections\\${profile.name}`; await (0, helpers_1.writeNetworkConfig)(pathname, profile); console.log(`Wrote network configuration for ${profile.name}`); } if (!options.connectionProfiles.length) { console.log('No network configuration provided'); } } finally { await diskpart.clearDriveLetter(bootPartition.volumeId, bootPartition.driveLetter); } } else { console.log(`\nSkip task: write configuration`); } if (tasks.includes('bootloader')) { // mount the boot partition and copy bootloader console.log('\nMount Windows boot partition and copy grub bootloader from image'); windows_commands_1.default.mountEfi(efiLabel); await (0, copy_bootloader_1.copyBootloaderFromImage)(imagePath, 1, BOOT_FILES_SOURCE_PATH, `${efiLabel !== null && efiLabel !== void 0 ? efiLabel : 'M'}:${BOOT_FILES_TARGET_PATH}`); console.log('Copied grub bootloader files'); // set boot file console.log('Set boot file'); const setBootResult = await windows_commands_1.default.setBoot(); console.log('Boot file set.', setBootResult); } else { console.log('\nSkip task: bootloader setup'); } if (tasks.includes('reboot')) { console.log('Migration complete, about to reboot'); windows_commands_1.default.shutdown.reboot(REBOOT_DELAY_SEC); } else { console.log('Skip task: reboot'); } } catch (error) { console.log("Can't proceed with migration:", error); return 1 /* MigrateResult.ERROR */; } return 0 /* MigrateResult.OK */; }; exports.migrate = migrate; //# sourceMappingURL=index.js.map