/** * @license * Copyright 2017-2021 Balena Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { flags } from '@oclif/command'; import type { LocalBalenaOsDevice } from 'balena-sync'; import Command from '../command'; import * as cf from '../utils/common-flags'; import { getCliUx, stripIndent } from '../utils/lazy'; interface FlagsDef { json?: boolean; verbose: boolean; timeout?: number; help: void; } export default class ScanCmd extends Command { public static description = stripIndent` Scan for balenaOS devices on your local network. Scan for balenaOS devices on your local network. The output includes device information collected through balenaEngine for devices running a development image of balenaOS. Devices running a production image do not expose balenaEngine (on TCP port 2375), which is why less information is printed about them. `; public static examples = [ '$ balena scan', '$ balena scan --timeout 120', '$ balena scan --verbose', ]; public static usage = 'scan'; public static flags: flags.Input = { verbose: flags.boolean({ default: false, char: 'v', description: 'display full info', }), timeout: flags.integer({ char: 't', description: 'scan timeout in seconds', }), help: cf.help, json: flags.boolean({ default: false, char: 'j', description: 'produce JSON output instead of tabular output', }), }; public static primary = true; public static root = true; public static offlineCompatible = true; public async run() { const _ = await import('lodash'); const { discover } = await import('balena-sync'); const prettyjson = await import('prettyjson'); const dockerUtils = await import('../utils/docker'); const dockerPort = 2375; const dockerTimeout = 2000; const { flags: options } = this.parse(ScanCmd); const discoverTimeout = options.timeout != null ? options.timeout * 1000 : undefined; // Find active local devices const ux = getCliUx(); ux.action.start('Scanning for local balenaOS devices'); const localDevices: LocalBalenaOsDevice[] = await discover.discoverLocalBalenaOsDevices(discoverTimeout); const engineReachableDevices: boolean[] = await Promise.all( localDevices.map(async ({ address }: { address: string }) => { const docker = await dockerUtils.createClient({ host: address, port: dockerPort, timeout: dockerTimeout, }); try { await docker.ping(); return true; } catch (err) { return false; } }), ); const developmentDevices: LocalBalenaOsDevice[] = localDevices.filter( (_localDevice, index) => engineReachableDevices[index], ); const productionDevices = _.differenceWith( localDevices, developmentDevices, _.isEqual, ); const productionDevicesInfo = _.map( productionDevices, (device: LocalBalenaOsDevice) => { return { host: device.host, address: device.address, osVariant: 'production', dockerInfo: undefined, dockerVersion: undefined, }; }, ); // Query devices for info const devicesInfo = await Promise.all( developmentDevices.map(async ({ host, address }) => { const docker = await dockerUtils.createClient({ host: address, port: dockerPort, timeout: dockerTimeout, }); const [dockerInfo, dockerVersion] = await Promise.all([ docker.info(), docker.version(), ]); return { host, address, osVariant: 'development', dockerInfo, dockerVersion, }; }), ); ux.action.stop('Reporting scan results'); // Reduce properties if not --verbose if (!options.verbose) { devicesInfo.forEach((d: any) => { d.dockerInfo = _.isObject(d.dockerInfo) ? _.pick(d.dockerInfo, ScanCmd.dockerInfoProperties) : d.dockerInfo; d.dockerVersion = _.isObject(d.dockerVersion) ? _.pick(d.dockerVersion, ScanCmd.dockerVersionProperties) : d.dockerVersion; }); } const cmdOutput: Array<{ host: string; address: string; osVariant: string; dockerInfo: any; dockerVersion: import('dockerode').DockerVersion | undefined; }> = [...productionDevicesInfo, ...devicesInfo]; // Output results if (!options.json && cmdOutput.length === 0) { console.error( process.platform === 'win32' ? ScanCmd.noDevicesFoundMessage + ScanCmd.windowsTipMessage : ScanCmd.noDevicesFoundMessage, ); return; } console.log( options.json ? JSON.stringify(cmdOutput, null, 4) : prettyjson.render(cmdOutput, { noColor: true }), ); } protected static dockerInfoProperties = [ 'Containers', 'ContainersRunning', 'ContainersPaused', 'ContainersStopped', 'Images', 'Driver', 'SystemTime', 'KernelVersion', 'OperatingSystem', 'Architecture', ]; protected static dockerVersionProperties = ['Version', 'ApiVersion']; protected static noDevicesFoundMessage = 'Could not find any balenaOS devices on the local network.'; protected static windowsTipMessage = ` Note for Windows users: The 'scan' command relies on the Bonjour service. Check whether Bonjour is installed (Control Panel > Programs and Features). If not, you can download Bonjour for Windows (included with Bonjour Print Services) from here: https://support.apple.com/kb/DL999 After installing Bonjour, restart your PC and run the 'balena scan' command again.`; }