2020-03-13 11:32:29 +00:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright 2020 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';
|
2020-02-27 12:38:50 +00:00
|
|
|
import type { LocalBalenaOsDevice } from 'balena-sync';
|
2020-03-13 11:32:29 +00:00
|
|
|
import Command from '../command';
|
|
|
|
import * as cf from '../utils/common-flags';
|
2020-11-20 00:16:47 +00:00
|
|
|
import { getCliUx, stripIndent } from '../utils/lazy';
|
2020-03-13 11:32:29 +00:00
|
|
|
|
|
|
|
interface FlagsDef {
|
2020-10-06 17:04:43 +00:00
|
|
|
json?: boolean;
|
2020-03-13 11:32:29 +00:00
|
|
|
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.
|
2020-11-30 12:50:21 +00:00
|
|
|
|
|
|
|
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.
|
2020-03-13 11:32:29 +00:00
|
|
|
`;
|
|
|
|
|
|
|
|
public static examples = [
|
|
|
|
'$ balena scan',
|
|
|
|
'$ balena scan --timeout 120',
|
|
|
|
'$ balena scan --verbose',
|
|
|
|
];
|
|
|
|
|
|
|
|
public static usage = 'scan';
|
|
|
|
|
|
|
|
public static flags: flags.Input<FlagsDef> = {
|
|
|
|
verbose: flags.boolean({
|
|
|
|
char: 'v',
|
|
|
|
default: false,
|
|
|
|
description: 'display full info',
|
|
|
|
}),
|
|
|
|
timeout: flags.integer({
|
|
|
|
char: 't',
|
|
|
|
description: 'scan timeout in seconds',
|
|
|
|
}),
|
|
|
|
help: cf.help,
|
2020-10-06 17:04:43 +00:00
|
|
|
json: flags.boolean({
|
|
|
|
char: 'j',
|
|
|
|
description: 'produce JSON output instead of tabular output',
|
|
|
|
}),
|
2020-03-13 11:32:29 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
public static primary = true;
|
|
|
|
public static root = 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');
|
|
|
|
|
2020-07-10 11:27:55 +00:00
|
|
|
const dockerPort = 2375;
|
|
|
|
const dockerTimeout = 2000;
|
|
|
|
|
2020-03-13 11:32:29 +00:00
|
|
|
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
|
|
|
|
|
|
|
|
const discoverTimeout =
|
|
|
|
options.timeout != null ? options.timeout * 1000 : undefined;
|
|
|
|
|
|
|
|
// Find active local devices
|
2020-11-20 00:16:47 +00:00
|
|
|
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 = dockerUtils.createClient({
|
|
|
|
host: address,
|
|
|
|
port: dockerPort,
|
|
|
|
timeout: dockerTimeout,
|
|
|
|
}) as any;
|
|
|
|
try {
|
|
|
|
await docker.pingAsync();
|
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
);
|
2020-11-30 12:50:21 +00:00
|
|
|
|
|
|
|
const developmentDevices: LocalBalenaOsDevice[] = localDevices.filter(
|
2020-11-20 00:16:47 +00:00
|
|
|
(_localDevice, index) => engineReachableDevices[index],
|
|
|
|
);
|
2020-03-13 11:32:29 +00:00
|
|
|
|
2020-11-30 12:50:21 +00:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2020-03-13 11:32:29 +00:00
|
|
|
// Query devices for info
|
2020-06-30 20:17:24 +00:00
|
|
|
const devicesInfo = await Promise.all(
|
2020-11-30 12:50:21 +00:00
|
|
|
developmentDevices.map(async ({ host, address }) => {
|
2020-03-13 11:32:29 +00:00
|
|
|
const docker = dockerUtils.createClient({
|
|
|
|
host: address,
|
|
|
|
port: dockerPort,
|
|
|
|
timeout: dockerTimeout,
|
|
|
|
}) as any;
|
2020-08-04 15:24:02 +00:00
|
|
|
const [dockerInfo, dockerVersion] = await Promise.all([
|
|
|
|
docker.infoAsync().catchReturn('Could not get Docker info'),
|
|
|
|
docker.versionAsync().catchReturn('Could not get Docker version'),
|
|
|
|
]);
|
|
|
|
return {
|
2020-03-13 11:32:29 +00:00
|
|
|
host,
|
|
|
|
address,
|
2020-11-30 12:50:21 +00:00
|
|
|
osVariant: 'development',
|
2020-08-04 15:24:02 +00:00
|
|
|
dockerInfo,
|
|
|
|
dockerVersion,
|
|
|
|
};
|
2020-06-30 20:17:24 +00:00
|
|
|
}),
|
2020-03-13 11:32:29 +00:00
|
|
|
);
|
|
|
|
|
2020-11-20 00:16:47 +00:00
|
|
|
ux.action.stop('Reporting scan results');
|
|
|
|
|
2020-03-13 11:32:29 +00:00
|
|
|
// 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;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-11-30 12:50:21 +00:00
|
|
|
const cmdOutput = productionDevicesInfo.concat(devicesInfo);
|
|
|
|
|
2020-03-13 11:32:29 +00:00
|
|
|
// Output results
|
2020-11-30 12:50:21 +00:00
|
|
|
if (!options.json && cmdOutput.length === 0) {
|
2020-11-20 00:16:47 +00:00
|
|
|
console.error(
|
|
|
|
process.platform === 'win32'
|
|
|
|
? ScanCmd.noDevicesFoundMessage + ScanCmd.windowsTipMessage
|
|
|
|
: ScanCmd.noDevicesFoundMessage,
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
2020-10-06 17:04:43 +00:00
|
|
|
console.log(
|
|
|
|
options.json
|
2020-11-30 12:50:21 +00:00
|
|
|
? JSON.stringify(cmdOutput, null, 4)
|
|
|
|
: prettyjson.render(cmdOutput, { noColor: true }),
|
2020-10-06 17:04:43 +00:00
|
|
|
);
|
2020-03-13 11:32:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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.`;
|
|
|
|
}
|