balena-cli
Version:
The official balena Command Line Interface
193 lines • 7.15 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDeviceOsRelease = exports.findBestUsernameForDevice = exports.isRootUserGood = exports.stdioIgnore = exports.RemoteCommandError = exports.SshPermissionDeniedError = void 0;
exports.sshArgsForRemoteCommand = sshArgsForRemoteCommand;
exports.runRemoteCommand = runRemoteCommand;
exports.getRemoteCommandOutput = getRemoteCommandOutput;
exports.getLocalDeviceCmdStdout = getLocalDeviceCmdStdout;
const child_process_1 = require("child_process");
const _ = require("lodash");
const errors_1 = require("../errors");
class SshPermissionDeniedError extends errors_1.ExpectedError {
}
exports.SshPermissionDeniedError = SshPermissionDeniedError;
class RemoteCommandError extends errors_1.ExpectedError {
constructor(cmd, exitCode, exitSignal) {
super(sshErrorMessage(cmd, exitSignal, exitCode));
this.cmd = cmd;
this.exitCode = exitCode;
this.exitSignal = exitSignal;
}
}
exports.RemoteCommandError = RemoteCommandError;
exports.stdioIgnore = {
stdin: 'ignore',
stdout: 'ignore',
stderr: 'ignore',
};
function sshArgsForRemoteCommand({ cmd = '', hostname, ignoreStdin = false, port, proxyCommand, username = 'root', verbose = false, }) {
port = port === 'local' ? 22222 : port === 'cloud' ? 22 : port;
return [
...(verbose ? ['-vvv'] : []),
...(ignoreStdin ? ['-n'] : []),
'-t',
...(port ? ['-p', port.toString()] : []),
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(proxyCommand && proxyCommand.length
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
: []),
`${username}@${hostname}`,
...(cmd ? [cmd] : []),
];
}
async function runRemoteCommand({ cmd = '', hostname, port, proxyCommand, stdin = 'inherit', stdout = 'inherit', stderr = 'inherit', username = 'root', verbose = false, }) {
let ignoreStdin;
if (stdin === 'ignore') {
ignoreStdin = true;
stdin = 'inherit';
}
else {
ignoreStdin = false;
}
const { which } = await Promise.resolve().then(() => require('./which'));
const program = await which('ssh');
const args = sshArgsForRemoteCommand({
cmd,
hostname,
ignoreStdin,
port,
proxyCommand,
username,
verbose,
});
if (process.env.DEBUG) {
const logger = (await Promise.resolve().then(() => require('./logger'))).getLogger();
logger.logDebug(`Executing [${program},${args}]`);
}
const stdio = [
typeof stdin === 'string' ? stdin : 'pipe',
typeof stdout === 'string' ? stdout : 'pipe',
typeof stderr === 'string' ? stderr : 'pipe',
];
let exitCode;
let exitSignal;
try {
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
const ps = (0, child_process_1.spawn)(program, args, { stdio })
.on('error', reject)
.on('close', (code, signal) => resolve([code !== null && code !== void 0 ? code : undefined, signal !== null && signal !== void 0 ? signal : undefined]));
if (ps.stdin && stdin && typeof stdin !== 'string') {
stdin.pipe(ps.stdin);
}
if (ps.stdout && stdout && typeof stdout !== 'string') {
ps.stdout.pipe(stdout);
}
if (ps.stderr && stderr && typeof stderr !== 'string') {
ps.stderr.pipe(stderr);
}
});
}
catch (error) {
const msg = [
`ssh failed with exit code=${exitCode} signal=${exitSignal}:`,
`[${program}, ${args.join(', ')}]`,
...(error ? [`${error}`] : []),
];
throw new errors_1.ExpectedError(msg.join('\n'));
}
if (exitCode || exitSignal) {
throw new RemoteCommandError(cmd, exitCode, exitSignal);
}
}
async function getRemoteCommandOutput({ cmd, hostname, port, proxyCommand, stdin = 'ignore', stdout = 'capture', stderr = 'capture', username = 'root', verbose = false, }) {
const { Writable } = await Promise.resolve().then(() => require('stream'));
const stdoutChunks = [];
const stderrChunks = [];
const stdoutStream = new Writable({
write(chunk, _enc, callback) {
stdoutChunks.push(chunk);
callback();
},
});
const stderrStream = new Writable({
write(chunk, _enc, callback) {
stderrChunks.push(chunk);
callback();
},
});
await runRemoteCommand({
cmd,
hostname,
port,
proxyCommand,
stdin,
stdout: stdout === 'capture' ? stdoutStream : stdout,
stderr: stderr === 'capture' ? stderrStream : stderr,
username,
verbose,
});
return {
stdout: Buffer.concat(stdoutChunks),
stderr: Buffer.concat(stderrChunks),
};
}
async function getLocalDeviceCmdStdout(hostname, cmd, stdout = 'capture') {
const port = 'local';
return (await getRemoteCommandOutput({
cmd,
hostname,
port,
stdout,
stderr: 'inherit',
username: await (0, exports.findBestUsernameForDevice)(hostname, port),
})).stdout;
}
exports.isRootUserGood = _.memoize(async (hostname, port) => {
try {
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...exports.stdioIgnore });
}
catch (e) {
return false;
}
return true;
});
exports.findBestUsernameForDevice = _.memoize(async (hostname, port) => {
var _a;
let username;
if (await (0, exports.isRootUserGood)(hostname, port)) {
username = 'root';
}
else {
const { getCachedUsername } = await Promise.resolve().then(() => require('./bootstrap'));
username = (_a = (await getCachedUsername())) === null || _a === void 0 ? void 0 : _a.username;
}
if (!username) {
const { stripIndent } = await Promise.resolve().then(() => require('./lazy'));
throw new errors_1.ExpectedError(stripIndent `
SSH authentication failed for 'root@${hostname}'.
Please login with 'balena login' for alternative authentication.`);
}
return username;
});
exports.getDeviceOsRelease = _.memoize(async (hostname) => (await getLocalDeviceCmdStdout(hostname, 'cat /etc/os-release')).toString());
function sshErrorMessage(cmd, exitSignal, exitCode) {
const msg = [];
cmd = cmd ? `Remote command "${cmd}"` : 'Process';
if (exitSignal) {
msg.push(`SSH: ${cmd} terminated with signal "${exitSignal}"`);
}
else {
msg.push(`SSH: ${cmd} exited with non-zero status code "${exitCode}"`);
switch (exitCode) {
case 255:
msg.push(`
Are the SSH keys correctly configured in balenaCloud? See:
https://www.balena.io/docs/learn/manage/ssh-access/#add-an-ssh-key-to-balenacloud`);
msg.push('Are you accidentally using `sudo`?');
}
}
return msg.join('\n');
}
//# sourceMappingURL=ssh.js.map
;