mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-20 17:33:18 +00:00
fix: Add single code path to get full, online-only device UUIDs
Both the tunnel and SSH commands require a full UUID for an online device. A single code path was added to provide this, taking either an application name or a partial UUID as a search parameter. In the event of an application name being provided, a device select form is presented to the user to pick from the online devices at that time. Change-type: patch Signed-off-by: Rich Bayliss <rich@balena.io>
This commit is contained in:
parent
2bbdfda92e
commit
5d137f3c20
@ -17,84 +17,8 @@ import * as BalenaSdk from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { BalenaApplicationNotFound, BalenaDeviceNotFound } from 'balena-errors';
|
||||
import {
|
||||
validateApplicationName,
|
||||
validateDotLocalUrl,
|
||||
validateIPAddress,
|
||||
validateShortUuid,
|
||||
validateUuid,
|
||||
} from '../utils/validation';
|
||||
|
||||
enum SSHTarget {
|
||||
APPLICATION,
|
||||
DEVICE,
|
||||
LOCAL_DEVICE,
|
||||
}
|
||||
|
||||
async function getSSHTarget(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
applicationOrDevice: string,
|
||||
): Promise<{
|
||||
target: SSHTarget;
|
||||
deviceChecked?: boolean;
|
||||
applicationChecked?: boolean;
|
||||
device?: BalenaSdk.Device;
|
||||
} | null> {
|
||||
if (
|
||||
validateDotLocalUrl(applicationOrDevice) ||
|
||||
validateIPAddress(applicationOrDevice)
|
||||
) {
|
||||
return { target: SSHTarget.LOCAL_DEVICE };
|
||||
}
|
||||
|
||||
const appTest = validateApplicationName(applicationOrDevice);
|
||||
const uuidTest = validateUuid(applicationOrDevice);
|
||||
if (appTest || uuidTest) {
|
||||
// Do some further processing to work out which it is
|
||||
if (appTest && !uuidTest) {
|
||||
return {
|
||||
target: SSHTarget.APPLICATION,
|
||||
applicationChecked: false,
|
||||
};
|
||||
}
|
||||
if (uuidTest && !appTest) {
|
||||
return {
|
||||
target: SSHTarget.DEVICE,
|
||||
deviceChecked: false,
|
||||
};
|
||||
}
|
||||
|
||||
// This is the harder part, we have a string that
|
||||
// fulfills both the uuid and application name
|
||||
// requirements. We should go away and test for both a
|
||||
// device with that uuid, and an application with that
|
||||
// name, and choose the appropriate one
|
||||
try {
|
||||
await sdk.models.application.get(applicationOrDevice);
|
||||
return { target: SSHTarget.APPLICATION, applicationChecked: true };
|
||||
} catch (e) {
|
||||
if (e instanceof BalenaApplicationNotFound) {
|
||||
// Here we want to check for a device with that UUID
|
||||
try {
|
||||
const device = await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['id', 'uuid', 'supervisor_version', 'is_online'],
|
||||
});
|
||||
return { target: SSHTarget.DEVICE, deviceChecked: true, device };
|
||||
} catch (err) {
|
||||
if (err instanceof BalenaDeviceNotFound) {
|
||||
throw new Error(
|
||||
`Device or application not found: ${applicationOrDevice}`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
import { BalenaDeviceNotFound } from 'balena-errors';
|
||||
import { validateDotLocalUrl, validateIPAddress } from '../utils/validation';
|
||||
|
||||
async function getContainerId(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
@ -297,17 +221,16 @@ export const ssh: CommandDefinition<
|
||||
},
|
||||
],
|
||||
action: async (params, options) => {
|
||||
const map = await import('lodash/map');
|
||||
const bash = await import('bash');
|
||||
// TODO: Make this typed
|
||||
const hasbin = require('hasbin');
|
||||
const { getSubShellCommand } = await import('../utils/helpers');
|
||||
const { child_process } = await import('mz');
|
||||
const { exitIfNotLoggedIn } = await import('../utils/patterns');
|
||||
|
||||
const { exitWithExpectedError, selectFromList } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const {
|
||||
exitIfNotLoggedIn,
|
||||
exitWithExpectedError,
|
||||
getOnlineTargetUuid,
|
||||
} = await import('../utils/patterns');
|
||||
const sdk = BalenaSdk.fromSharedOptions();
|
||||
|
||||
const verbose = options.verbose === true;
|
||||
@ -316,7 +239,45 @@ export const ssh: CommandDefinition<
|
||||
const useProxy = !!proxyConfig && !options.noProxy;
|
||||
const port = options.port != null ? parseInt(options.port, 10) : undefined;
|
||||
|
||||
const getSshProxyCommand = (hasTunnelBin: boolean) => {
|
||||
// if we're doing a direct SSH connection locally...
|
||||
if (
|
||||
validateDotLocalUrl(params.applicationOrDevice) ||
|
||||
validateIPAddress(params.applicationOrDevice)
|
||||
) {
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
address: params.applicationOrDevice,
|
||||
port,
|
||||
verbose,
|
||||
service: params.serviceName,
|
||||
});
|
||||
}
|
||||
|
||||
// this will be a tunnelled SSH connection...
|
||||
exitIfNotLoggedIn();
|
||||
const uuid = await getOnlineTargetUuid(sdk, params.applicationOrDevice);
|
||||
let version: string | undefined;
|
||||
let id: number | undefined;
|
||||
|
||||
try {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'supervisor_version', 'is_online'],
|
||||
});
|
||||
id = device.id;
|
||||
version = device.supervisor_version;
|
||||
} catch (e) {
|
||||
if (e instanceof BalenaDeviceNotFound) {
|
||||
exitWithExpectedError(`Could not find device: ${uuid}`);
|
||||
}
|
||||
}
|
||||
|
||||
const [hasTunnelBin, username, proxyUrl] = await Promise.all([
|
||||
useProxy ? await hasbin('proxytunnel') : undefined,
|
||||
sdk.auth.whoami(),
|
||||
sdk.settings.get('proxyUrl'),
|
||||
]);
|
||||
|
||||
const getSshProxyCommand = () => {
|
||||
if (!useProxy) {
|
||||
return '';
|
||||
}
|
||||
@ -346,159 +307,57 @@ export const ssh: CommandDefinition<
|
||||
};
|
||||
}
|
||||
|
||||
const proxyCommand = `proxytunnel ${bash.args(tunnelOptions, '--', '=')}`;
|
||||
return `-o ${bash.args({ ProxyCommand: proxyCommand }, '', '=')}`;
|
||||
const ProxyCommand = `proxytunnel ${bash.args(tunnelOptions, '--', '=')}`;
|
||||
return `-o ${bash.args({ ProxyCommand }, '', '=')}`;
|
||||
};
|
||||
|
||||
// Detect what type of SSH we're doing
|
||||
const maybeParamChecks = await getSSHTarget(
|
||||
sdk,
|
||||
params.applicationOrDevice,
|
||||
);
|
||||
if (maybeParamChecks == null) {
|
||||
const proxyCommand = getSshProxyCommand();
|
||||
|
||||
if (username == null) {
|
||||
exitWithExpectedError(
|
||||
new Error(stripIndent`
|
||||
Could not parse SSH target.
|
||||
You can provide an application name, IP address or .local address`),
|
||||
`Opening an SSH connection to a remote device requires you to be logged in.`,
|
||||
);
|
||||
}
|
||||
const paramChecks = maybeParamChecks!;
|
||||
|
||||
switch (paramChecks.target) {
|
||||
case SSHTarget.APPLICATION:
|
||||
exitIfNotLoggedIn();
|
||||
// Here what we want to do is fetch all device which
|
||||
// are part of this application, and online
|
||||
try {
|
||||
const devices = await sdk.models.device.getAllByApplication(
|
||||
params.applicationOrDevice,
|
||||
{ $filter: { is_online: true }, $select: ['device_name', 'uuid'] },
|
||||
);
|
||||
const choice = await selectFromList(
|
||||
'Please choose an online device to SSH into:',
|
||||
map(devices, ({ device_name, uuid: uuidToChoose }) => ({
|
||||
name: `${device_name} [${uuidToChoose.substr(0, 7)}]`,
|
||||
uuid: uuidToChoose,
|
||||
})),
|
||||
);
|
||||
// A little bit hacky, but it means we can fall
|
||||
// through to the next handling mechanism
|
||||
params.applicationOrDevice = choice.uuid;
|
||||
} catch (e) {
|
||||
if (e instanceof BalenaApplicationNotFound) {
|
||||
exitWithExpectedError(
|
||||
`Could not find an application named ${
|
||||
params.applicationOrDevice
|
||||
}`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
case SSHTarget.DEVICE:
|
||||
exitIfNotLoggedIn();
|
||||
// We want to do two things here; firstly, check
|
||||
// that the device exists and is accessible, and
|
||||
// also convert a short uuid to a long one if
|
||||
// necessary
|
||||
let uuid = params.applicationOrDevice;
|
||||
let version: string | undefined;
|
||||
let id: number | undefined;
|
||||
let isOnline: boolean | undefined;
|
||||
// We also want to avoid checking for a device if we
|
||||
// know it exists
|
||||
if (!paramChecks.deviceChecked || validateShortUuid(uuid)) {
|
||||
try {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'uuid', 'supervisor_version', 'is_online'],
|
||||
});
|
||||
uuid = device.uuid;
|
||||
version = device.supervisor_version;
|
||||
id = device.id;
|
||||
isOnline = device.is_online;
|
||||
} catch (e) {
|
||||
if (e instanceof BalenaDeviceNotFound) {
|
||||
exitWithExpectedError(`Could not find device: ${uuid}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
version = paramChecks.device!.supervisor_version;
|
||||
uuid = paramChecks.device!.uuid;
|
||||
id = paramChecks.device!.id;
|
||||
isOnline = paramChecks.device!.is_online;
|
||||
}
|
||||
|
||||
if (!isOnline) {
|
||||
throw new Error(`Device ${uuid} is not online.`);
|
||||
}
|
||||
|
||||
const [hasTunnelBin, username, proxyUrl] = await Promise.all([
|
||||
useProxy ? await hasbin('proxytunnel') : undefined,
|
||||
sdk.auth.whoami(),
|
||||
sdk.settings.get('proxyUrl'),
|
||||
]);
|
||||
const proxyCommand = getSshProxyCommand(hasTunnelBin);
|
||||
|
||||
if (username == null) {
|
||||
exitWithExpectedError(
|
||||
`Opening an SSH connection to a remote device requires you to be logged in.`,
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, we have a long uuid with a device
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.serviceName != null) {
|
||||
containerId = await getContainerId(
|
||||
sdk,
|
||||
uuid,
|
||||
params.serviceName,
|
||||
{
|
||||
port,
|
||||
proxyCommand,
|
||||
proxyUrl,
|
||||
username: username!,
|
||||
},
|
||||
version,
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
let accessCommand: string;
|
||||
if (containerId != null) {
|
||||
accessCommand = `enter ${uuid} ${containerId}`;
|
||||
} else {
|
||||
accessCommand = `host ${uuid}`;
|
||||
}
|
||||
|
||||
const command = generateVpnSshCommand({
|
||||
uuid,
|
||||
command: accessCommand,
|
||||
verbose,
|
||||
// At this point, we have a long uuid with a device
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.serviceName != null) {
|
||||
containerId = await getContainerId(
|
||||
sdk,
|
||||
uuid,
|
||||
params.serviceName,
|
||||
{
|
||||
port,
|
||||
proxyCommand,
|
||||
proxyUrl,
|
||||
username: username!,
|
||||
});
|
||||
|
||||
const subShellCommand = getSubShellCommand(command);
|
||||
await child_process.spawn(
|
||||
subShellCommand.program,
|
||||
subShellCommand.args,
|
||||
{
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
|
||||
break;
|
||||
case SSHTarget.LOCAL_DEVICE:
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
await performLocalDeviceSSH({
|
||||
address: params.applicationOrDevice,
|
||||
port,
|
||||
verbose,
|
||||
service: params.serviceName,
|
||||
});
|
||||
break;
|
||||
},
|
||||
version,
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
let accessCommand: string;
|
||||
if (containerId != null) {
|
||||
accessCommand = `enter ${uuid} ${containerId}`;
|
||||
} else {
|
||||
accessCommand = `host ${uuid}`;
|
||||
}
|
||||
|
||||
const command = generateVpnSshCommand({
|
||||
uuid,
|
||||
command: accessCommand,
|
||||
verbose,
|
||||
port,
|
||||
proxyCommand,
|
||||
proxyUrl,
|
||||
username: username!,
|
||||
});
|
||||
|
||||
const subShellCommand = getSubShellCommand(command);
|
||||
await child_process.spawn(subShellCommand.program, subShellCommand.args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -20,24 +20,17 @@ import * as _ from 'lodash';
|
||||
import { createServer, Server, Socket } from 'net';
|
||||
import { isArray } from 'util';
|
||||
|
||||
import { inferOrSelectDevice } from '../utils/patterns';
|
||||
import { getOnlineTargetUuid } from '../utils/patterns';
|
||||
import { tunnelConnectionToDevice } from '../utils/tunnel';
|
||||
|
||||
interface Args {
|
||||
uuid: string;
|
||||
deviceOrApplication: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
port: string | string[];
|
||||
}
|
||||
|
||||
class DeviceIsOfflineError extends Error {
|
||||
public uuid: string;
|
||||
constructor(uuid: string) {
|
||||
super(`Device '${uuid}' is offline`);
|
||||
this.uuid = uuid;
|
||||
}
|
||||
}
|
||||
class InvalidPortMappingError extends Error {
|
||||
constructor(mapping: string) {
|
||||
super(`'${mapping}' is not a valid port mapping.`);
|
||||
@ -56,7 +49,7 @@ const isValidPort = (port: number) => {
|
||||
};
|
||||
|
||||
export const tunnel: CommandDefinition<Args, Options> = {
|
||||
signature: 'tunnel <uuid>',
|
||||
signature: 'tunnel <deviceOrApplication>',
|
||||
description: 'Tunnel local ports to your balenaOS device',
|
||||
help: stripIndent`
|
||||
Use this command to open local ports which tunnel to listening ports on your balenaOS device.
|
||||
@ -94,7 +87,7 @@ export const tunnel: CommandDefinition<Args, Options> = {
|
||||
|
||||
primary: true,
|
||||
|
||||
async action(params, options, done) {
|
||||
action: async (params, options) => {
|
||||
const Logger = await import('../utils/logger');
|
||||
const logger = new Logger();
|
||||
const balena = await import('balena-sdk');
|
||||
@ -118,8 +111,6 @@ export const tunnel: CommandDefinition<Args, Options> = {
|
||||
}
|
||||
};
|
||||
|
||||
logger.logInfo(`Tunnel to ${params.uuid}`);
|
||||
|
||||
if (options.port === undefined) {
|
||||
throw new NoPortsDefinedError();
|
||||
}
|
||||
@ -129,117 +120,111 @@ export const tunnel: CommandDefinition<Args, Options> = {
|
||||
? (options.port as string[])
|
||||
: [options.port as string];
|
||||
|
||||
return inferOrSelectDevice(params.uuid)
|
||||
.then(sdk.models.device.get)
|
||||
.then(device => {
|
||||
if (!device.is_online) {
|
||||
throw new DeviceIsOfflineError(device.uuid);
|
||||
const uuid = await getOnlineTargetUuid(sdk, params.deviceOrApplication);
|
||||
const device = await sdk.models.device.get(uuid);
|
||||
|
||||
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
||||
|
||||
const localListeners = _.chain(ports)
|
||||
.map(mapping => {
|
||||
const regexResult = /^([0-9]+)(?:$|\:(?:([\w\:\.]+)\:|)([0-9]+))$/.exec(
|
||||
mapping,
|
||||
);
|
||||
|
||||
if (regexResult === null) {
|
||||
throw new InvalidPortMappingError(mapping);
|
||||
}
|
||||
|
||||
const localListeners = _.chain(ports)
|
||||
.map(mapping => {
|
||||
const regexResult = /^([0-9]+)(?:$|\:(?:([\w\:\.]+)\:|)([0-9]+))$/.exec(
|
||||
mapping,
|
||||
// grab the groups
|
||||
// tslint:disable-next-line:prefer-const
|
||||
let [, remotePort, localAddress, localPort] = regexResult;
|
||||
|
||||
if (
|
||||
!isValidPort(parseInt(localPort, undefined)) ||
|
||||
!isValidPort(parseInt(remotePort, undefined))
|
||||
) {
|
||||
throw new InvalidPortMappingError(mapping);
|
||||
}
|
||||
|
||||
// default bind to localAddress
|
||||
if (localAddress == null) {
|
||||
localAddress = 'localhost';
|
||||
}
|
||||
|
||||
// default use same port number locally as remote
|
||||
if (localPort == null) {
|
||||
localPort = remotePort;
|
||||
}
|
||||
|
||||
return {
|
||||
localPort: parseInt(localPort, undefined),
|
||||
localAddress,
|
||||
remotePort: parseInt(remotePort, undefined),
|
||||
};
|
||||
})
|
||||
.map(({ localPort, localAddress, remotePort }) => {
|
||||
return tunnelConnectionToDevice(device.uuid, remotePort, sdk)
|
||||
.then(handler =>
|
||||
createServer((client: Socket) => {
|
||||
return handler(client)
|
||||
.then(() => {
|
||||
logConnection(
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
);
|
||||
})
|
||||
.catch(err =>
|
||||
logConnection(
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
err,
|
||||
),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.then(
|
||||
server =>
|
||||
new Bluebird.Promise<Server>((resolve, reject) => {
|
||||
server.on('error', reject);
|
||||
server.listen(localPort, localAddress, () => {
|
||||
resolve(server);
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
logger.logInfo(
|
||||
` - tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}`,
|
||||
);
|
||||
|
||||
if (regexResult === null) {
|
||||
throw new InvalidPortMappingError(mapping);
|
||||
}
|
||||
|
||||
// grab the groups
|
||||
// tslint:disable-next-line:prefer-const
|
||||
let [, remotePort, localAddress, localPort] = regexResult;
|
||||
|
||||
if (
|
||||
!isValidPort(parseInt(localPort, undefined)) ||
|
||||
!isValidPort(parseInt(remotePort, undefined))
|
||||
) {
|
||||
throw new InvalidPortMappingError(mapping);
|
||||
}
|
||||
|
||||
// default bind to localAddress
|
||||
if (localAddress == null) {
|
||||
localAddress = 'localhost';
|
||||
}
|
||||
|
||||
// default use same port number locally as remote
|
||||
if (localPort == null) {
|
||||
localPort = remotePort;
|
||||
}
|
||||
|
||||
return {
|
||||
localPort: parseInt(localPort, undefined),
|
||||
localAddress,
|
||||
remotePort: parseInt(remotePort, undefined),
|
||||
};
|
||||
return true;
|
||||
})
|
||||
.map(({ localPort, localAddress, remotePort }) => {
|
||||
return tunnelConnectionToDevice(device.uuid, remotePort, sdk)
|
||||
.then(handler =>
|
||||
createServer((client: Socket) => {
|
||||
return handler(client)
|
||||
.then(() => {
|
||||
logConnection(
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
);
|
||||
})
|
||||
.catch(err =>
|
||||
logConnection(
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
err,
|
||||
),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.then(
|
||||
server =>
|
||||
new Bluebird.Promise<Server>((resolve, reject) => {
|
||||
server.on('error', reject);
|
||||
server.listen(localPort, localAddress, () => {
|
||||
resolve(server);
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
logger.logInfo(
|
||||
` - tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}`,
|
||||
);
|
||||
.catch((err: Error) => {
|
||||
logger.logWarn(
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logger.logWarn(
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
|
||||
);
|
||||
|
||||
return false;
|
||||
});
|
||||
})
|
||||
.value();
|
||||
|
||||
return Bluebird.all(localListeners);
|
||||
return false;
|
||||
});
|
||||
})
|
||||
.then(results => {
|
||||
if (!results.includes(true)) {
|
||||
throw new Error('No ports are valid for tunnelling');
|
||||
}
|
||||
.value();
|
||||
|
||||
logger.logInfo('Waiting for connections...');
|
||||
})
|
||||
.nodeify(done);
|
||||
const results = await Promise.all(localListeners);
|
||||
if (!results.includes(true)) {
|
||||
throw new Error('No ports are valid for tunnelling');
|
||||
}
|
||||
|
||||
logger.logInfo('Waiting for connections...');
|
||||
},
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ 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 { BalenaApplicationNotFound } from 'balena-errors';
|
||||
import BalenaSdk = require('balena-sdk');
|
||||
import Bluebird = require('bluebird');
|
||||
import chalk from 'chalk';
|
||||
@ -283,6 +284,61 @@ export function inferOrSelectDevice(preferredUuid: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getOnlineTargetUuid(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
applicationOrDevice: string,
|
||||
) {
|
||||
const appTest = validation.validateApplicationName(applicationOrDevice);
|
||||
const uuidTest = validation.validateUuid(applicationOrDevice);
|
||||
|
||||
if (!appTest && !uuidTest) {
|
||||
throw new Error(`Device or application not found: ${applicationOrDevice}`);
|
||||
}
|
||||
|
||||
// if we have a definite device UUID...
|
||||
if (uuidTest && !appTest) {
|
||||
return (await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['uuid'],
|
||||
$filter: { is_online: true },
|
||||
})).uuid;
|
||||
}
|
||||
|
||||
// otherwise, it may be a device OR an application...
|
||||
try {
|
||||
const app = await sdk.models.application.get(applicationOrDevice);
|
||||
const devices = await sdk.models.device.getAllByApplication(app.id, {
|
||||
$filter: { is_online: true },
|
||||
});
|
||||
|
||||
if (_.isEmpty(devices)) {
|
||||
throw new Error('No accessible devices are online');
|
||||
}
|
||||
|
||||
return await getForm().ask({
|
||||
message: 'Select a device',
|
||||
type: 'list',
|
||||
default: devices[0].uuid,
|
||||
choices: _.map(devices, device => ({
|
||||
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(
|
||||
0,
|
||||
7,
|
||||
)})`,
|
||||
value: device.uuid,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
if (!(err instanceof BalenaApplicationNotFound)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// it wasn't an application, maybe it's a device...
|
||||
return (await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['uuid'],
|
||||
$filter: { is_online: true },
|
||||
})).uuid;
|
||||
}
|
||||
|
||||
export function selectFromList<T>(
|
||||
message: string,
|
||||
choices: Array<T & { name: string }>,
|
||||
|
Loading…
x
Reference in New Issue
Block a user