device move: Add ability to move multiple devices in one command

Change-type: minor
This commit is contained in:
Nick 2020-07-15 08:26:31 -06:00
parent d419ae9183
commit edaf76e329
No known key found for this signature in database
GPG Key ID: 406BEDEB8C2FD516
6 changed files with 77 additions and 40 deletions

View File

@ -170,7 +170,7 @@ Users are encouraged to regularly update the balena CLI to the latest version.
- [device identify <uuid>](#device-identify-uuid)
- [device init](#device-init)
- [device <uuid>](#device-uuid)
- [device move <uuid>](#device-move-uuid)
- [device move <uuid(s)>](#device-move-uuid-s)
- [device reboot <uuid>](#device-reboot-uuid)
- [device register <application>](#device-register-application)
- [device rename <uuid> [newname]](#device-rename-uuid-newname)
@ -543,9 +543,9 @@ the device uuid
### Options
## device move <uuid>
## device move <uuid(s)>
Move a device to another application.
Move one or more devices to another application.
Note, if the application option is omitted it will be prompted
for interactively.
@ -553,13 +553,14 @@ for interactively.
Examples:
$ balena device move 7cf02a6
$ balena device move 7cf02a6,dc39e52
$ balena device move 7cf02a6 --application MyNewApp
### Arguments
#### UUID
the uuid of the device to move
comma-separated list (no blank spaces) of device UUIDs to be moved
### Options

View File

@ -23,6 +23,7 @@ import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface ExtendedDevice extends Device {
application_name?: string;
@ -40,28 +41,30 @@ interface ArgsDef {
export default class DeviceMoveCmd extends Command {
public static description = stripIndent`
Move a device to another application.
Move one or more devices to another application.
Move a device to another application.
Move one or more devices to another application.
Note, if the application option is omitted it will be prompted
for interactively.
`;
public static examples = [
'$ balena device move 7cf02a6',
'$ balena device move 7cf02a6,dc39e52',
'$ balena device move 7cf02a6 --application MyNewApp',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to move',
description:
'comma-separated list (no blank spaces) of device UUIDs to be moved',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
public static usage = 'device move <uuid>';
public static usage = 'device move <uuid(s)>';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
@ -82,47 +85,72 @@ export default class DeviceMoveCmd extends Command {
options.application = options.application || options.app;
delete options.app;
const device: ExtendedDevice = await balena.models.device.get(
params.uuid,
expandForAppName,
const devices: ExtendedDevice[] = await Promise.all(
params.uuid
.split(',')
.map((uuid) => balena.models.device.get(uuid, expandForAppName)),
);
const belongsToApplication = device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
for (const device of devices) {
const belongsToApplication = device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
}
// Get destination application
let application;
if (options.application) {
application = options.application;
} else {
const [deviceDeviceType, deviceTypes] = await Promise.all([
balena.models.device.getManifestBySlug(device.device_type),
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
Promise.all(
devices.map((device) =>
balena.models.device.getManifestBySlug(device.device_type),
),
),
balena.models.config.getDeviceTypes(),
]);
const compatibleDeviceTypes = deviceTypes.filter(
(dt) =>
balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceDeviceType.isDependent &&
dt.state !== 'DISCONTINUED',
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
deviceDeviceTypes.every(
(deviceDeviceType) =>
balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceDeviceType.isDependent &&
dt.state !== 'DISCONTINUED',
),
);
const patterns = await import('../../utils/patterns');
application = await patterns.selectApplication(
(app: Application) =>
compatibleDeviceTypes.some((dt) => dt.slug === app.device_type) &&
// @ts-ignore using the extended device object prop
device.application_name !== app.app_name,
);
try {
application = await patterns.selectApplication(
(app: Application) =>
compatibleDeviceTypes.some((dt) => dt.slug === app.device_type) &&
// @ts-ignore using the extended device object prop
devices.some((device) => device.application_name !== app.app_name),
true,
);
} catch (err) {
if (deviceDeviceTypes.length) {
throw new ExpectedError(
`${err.message}\nDo all devices have a compatible architecture?`,
);
}
throw err;
}
}
await balena.models.device.move(params.uuid, tryAsInteger(application));
console.info(`${params.uuid} was moved to ${application}`);
for (const uuid of params.uuid.split(',')) {
try {
await balena.models.device.move(uuid, tryAsInteger(application));
console.info(`${uuid} was moved to ${application}`);
} catch (err) {
console.info(`${err.message}, uuid: ${uuid}`);
process.exitCode = 1;
}
}
}
}

View File

@ -147,6 +147,7 @@ const EXPECTED_ERROR_REGEXES = [
/^BalenaApplicationNotFound/, // balena-sdk
/^BalenaDeviceNotFound/, // balena-sdk
/^BalenaExpiredToken/, // balena-sdk
/^BalenaInvalidDeviceType/, // balena-sdk
/^Missing \w+$/, // Capitano,
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
/Missing required flag/, // oclif parser: RequiredFlagError

View File

@ -160,6 +160,7 @@ export async function confirm(
export function selectApplication(
filter?: (app: BalenaSdk.Application) => boolean,
errorOnEmptySelection = false,
) {
const balena = getBalenaSdk();
return balena.models.application
@ -173,6 +174,9 @@ export function selectApplication(
})
.filter(filter || _.constant(true))
.then((applications) => {
if (errorOnEmptySelection && applications.length === 0) {
throw new ExpectedError('No suitable applications found for selection');
}
return getCliForm().ask({
message: 'Select an application',
type: 'list',

View File

@ -20,13 +20,13 @@ import { BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
const HELP_RESPONSE = `
Move a device to another application.
Move one or more devices to another application.
USAGE
$ balena device move <uuid>
$ balena device move <uuid(s)>
ARGUMENTS
<uuid> the uuid of the device to move
<uuid> comma-separated list (no blank spaces) of device UUIDs to be moved
OPTIONS
-a, --application <application> application name
@ -34,13 +34,14 @@ OPTIONS
--app <app> same as '--application'
DESCRIPTION
Move a device to another application.
Move one or more devices to another application.
Note, if the application option is omitted it will be prompted
for interactively.
EXAMPLES
$ balena device move 7cf02a6
$ balena device move 7cf02a6,dc39e52
$ balena device move 7cf02a6 --application MyNewApp
`;
@ -71,7 +72,9 @@ describe('balena device move', function () {
const errLines = cleanOutput(err);
expect(errLines[0]).to.equal('Missing 1 required argument:');
expect(errLines[1]).to.equal('uuid : the uuid of the device to move');
expect(errLines[1]).to.equal(
'uuid : comma-separated list (no blank spaces) of device UUIDs to be moved',
);
expect(out).to.eql([]);
});

View File

@ -58,7 +58,7 @@ Additional commands:
config write <key> <value> write a device configuration
device identify <uuid> identify a device
device init initialise a device with balenaOS
device move <uuid> move a device to another application
device move <uuid(s)> move one or more devices to another application
device os-update <uuid> start a Host OS update for a device
device public-url <uuid> get or manage the public URL for a device
device reboot <uuid> restart a device