mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-29 01:58:52 +00:00
9ec37975f3
Resolves: #2500 Change-type: major Signed-off-by: Matthew Yarmolinsky <matthew-timothy@balena.io>
232 lines
7.2 KiB
TypeScript
232 lines
7.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2016-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';
|
|
import Command from '../command';
|
|
import * as cf from '../utils/common-flags';
|
|
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
|
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
|
|
|
|
interface FlagsDef {
|
|
port?: number;
|
|
tty: boolean;
|
|
verbose: boolean;
|
|
noproxy: boolean;
|
|
help: void;
|
|
}
|
|
|
|
interface ArgsDef {
|
|
fleetOrDevice: string;
|
|
service?: string;
|
|
}
|
|
|
|
export default class SshCmd extends Command {
|
|
public static description = stripIndent`
|
|
Open a SSH prompt on a device's host OS or service container.
|
|
|
|
Start a shell on a local or remote device. If a service name is not provided,
|
|
a shell will be opened on the host OS.
|
|
|
|
If a fleet is provided, an interactive menu will be presented for the selection
|
|
of an online device. A shell will then be opened for the host OS or service
|
|
container of the chosen device.
|
|
|
|
For local devices, the IP address and .local domain name are supported.
|
|
If the device is referenced by IP or \`.local\` address, the connection
|
|
is initiated directly to balenaOS on port \`22222\` via an
|
|
openssh-compatible client. Otherwise, any connection initiated remotely
|
|
traverses the balenaCloud VPN.
|
|
|
|
Commands may be piped to the standard input for remote execution (see examples).
|
|
Note however that remote command execution on service containers (as opposed to
|
|
the host OS) is not currently possible when a device UUID is used (instead of
|
|
an IP address) because of a balenaCloud backend limitation.
|
|
|
|
Note: \`balena ssh\` requires an openssh-compatible client to be correctly
|
|
installed in your shell environment. For more information (including Windows
|
|
support) please check:
|
|
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies,
|
|
`;
|
|
|
|
public static examples = [
|
|
'$ balena ssh MyFleet',
|
|
'$ balena ssh f49cefd',
|
|
'$ balena ssh f49cefd my-service',
|
|
'$ balena ssh f49cefd --port <port>',
|
|
'$ balena ssh 192.168.0.1 --verbose',
|
|
'$ balena ssh f49cefd.local my-service',
|
|
'$ echo "uptime; exit;" | balena ssh f49cefd',
|
|
'$ echo "uptime; exit;" | balena ssh 192.168.0.1 myService',
|
|
];
|
|
|
|
public static args = [
|
|
{
|
|
name: 'fleetOrDevice',
|
|
description: 'fleet name/slug, device uuid, or address of local device',
|
|
required: true,
|
|
},
|
|
{
|
|
name: 'service',
|
|
description: 'service name, if connecting to a container',
|
|
required: false,
|
|
},
|
|
];
|
|
|
|
public static usage = 'ssh <fleetOrDevice> [service]';
|
|
|
|
public static flags: flags.Input<FlagsDef> = {
|
|
port: flags.integer({
|
|
description: stripIndent`
|
|
SSH server port number (default 22222) if the target is an IP address or .local
|
|
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
|
|
char: 'p',
|
|
parse: (p) => parseAsInteger(p, 'port'),
|
|
}),
|
|
tty: flags.boolean({
|
|
default: false,
|
|
description:
|
|
'force pseudo-terminal allocation (bypass TTY autodetection for stdin)',
|
|
char: 't',
|
|
}),
|
|
verbose: flags.boolean({
|
|
default: false,
|
|
description: 'increase verbosity',
|
|
char: 'v',
|
|
}),
|
|
noproxy: flags.boolean({
|
|
default: false,
|
|
description: 'bypass global proxy configuration for the ssh connection',
|
|
}),
|
|
help: cf.help,
|
|
};
|
|
|
|
public static primary = true;
|
|
public static offlineCompatible = true;
|
|
|
|
public async run() {
|
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
|
SshCmd,
|
|
);
|
|
|
|
// Local connection
|
|
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
|
|
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
|
return await performLocalDeviceSSH({
|
|
hostname: params.fleetOrDevice,
|
|
port: options.port || 'local',
|
|
forceTTY: options.tty,
|
|
verbose: options.verbose,
|
|
service: params.service,
|
|
});
|
|
}
|
|
|
|
// Remote connection
|
|
const { getProxyConfig } = await import('../utils/helpers');
|
|
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
|
const sdk = getBalenaSdk();
|
|
|
|
const proxyConfig = getProxyConfig();
|
|
const useProxy = !!proxyConfig && !options.noproxy;
|
|
|
|
// this will be a tunnelled SSH connection...
|
|
await Command.checkNotUsingOfflineMode();
|
|
await Command.checkLoggedIn();
|
|
const deviceUuid = await getOnlineTargetDeviceUuid(
|
|
sdk,
|
|
params.fleetOrDevice,
|
|
);
|
|
|
|
const { which } = await import('../utils/which');
|
|
|
|
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
|
|
useProxy ? which('proxytunnel', false) : undefined,
|
|
sdk.auth.whoami(),
|
|
// note that `proxyUrl` refers to the balenaCloud "resin-proxy"
|
|
// service, currently "balena-devices.com", rather than some
|
|
// local proxy server URL
|
|
sdk.settings.get('proxyUrl'),
|
|
]);
|
|
|
|
const getSshProxyCommand = () => {
|
|
if (!proxyConfig) {
|
|
return;
|
|
}
|
|
if (!whichProxytunnel) {
|
|
console.warn(stripIndent`
|
|
Proxy is enabled but the \`proxytunnel\` binary cannot be found.
|
|
Please install it if you want to route the \`balena ssh\` requests through the proxy.
|
|
Alternatively you can pass \`--noproxy\` param to the \`balena ssh\` command to ignore the proxy config
|
|
for the \`ssh\` requests.
|
|
|
|
Attempting the unproxied request for now.`);
|
|
return;
|
|
}
|
|
|
|
const p = proxyConfig;
|
|
if (p.username && p.password) {
|
|
// proxytunnel understands these variables for proxy authentication.
|
|
// Setting the variables instead of command-line options avoids the
|
|
// need for shell-specific escaping of special characters like '$'.
|
|
process.env.PROXYUSER = p.username;
|
|
process.env.PROXYPASS = p.password;
|
|
}
|
|
|
|
return [
|
|
'proxytunnel',
|
|
`--proxy=${p.host}:${p.port}`,
|
|
// ssh replaces these %h:%p variables in the ProxyCommand option
|
|
// https://linux.die.net/man/5/ssh_config
|
|
'--dest=%h:%p',
|
|
...(options.verbose ? ['--verbose'] : []),
|
|
];
|
|
};
|
|
|
|
const proxyCommand = useProxy ? getSshProxyCommand() : undefined;
|
|
|
|
// At this point, we have a long uuid of a device
|
|
// that we know exists and is accessible
|
|
let containerId: string | undefined;
|
|
if (params.service != null) {
|
|
const { getContainerIdForService } = await import('../utils/device/ssh');
|
|
containerId = await getContainerIdForService({
|
|
deviceUuid,
|
|
hostname: `ssh.${proxyUrl}`,
|
|
port: options.port || 'cloud',
|
|
proxyCommand,
|
|
service: params.service,
|
|
username: username!,
|
|
});
|
|
}
|
|
|
|
let accessCommand: string;
|
|
if (containerId != null) {
|
|
accessCommand = `enter ${deviceUuid} ${containerId}`;
|
|
} else {
|
|
accessCommand = `host ${deviceUuid}`;
|
|
}
|
|
const { runRemoteCommand } = await import('../utils/ssh');
|
|
await runRemoteCommand({
|
|
cmd: accessCommand,
|
|
hostname: `ssh.${proxyUrl}`,
|
|
port: options.port || 'cloud',
|
|
proxyCommand,
|
|
username,
|
|
verbose: options.verbose,
|
|
});
|
|
}
|
|
}
|