Merge pull request #1871 from balena-io/convert-devices

Convert device commands to oclif
This commit is contained in:
bulldozer-balena[bot] 2020-06-18 19:30:57 +00:00 committed by GitHub
commit 795057338f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1129 additions and 531 deletions

View File

@ -51,6 +51,15 @@ const capitanoDoc = {
title: 'Device', title: 'Device',
files: [ files: [
'build/actions/device.js', 'build/actions/device.js',
'build/actions-oclif/device/identify.js',
'build/actions-oclif/device/index.js',
'build/actions-oclif/device/move.js',
'build/actions-oclif/device/reboot.js',
'build/actions-oclif/device/register.js',
'build/actions-oclif/device/rename.js',
'build/actions-oclif/device/rm.js',
'build/actions-oclif/device/shutdown.js',
'build/actions-oclif/devices/index.js',
'build/actions-oclif/devices/supported.js', 'build/actions-oclif/devices/supported.js',
'build/actions-oclif/device/public-url.js', 'build/actions-oclif/device/public-url.js',
], ],

View File

@ -167,17 +167,17 @@ Users are encouraged to regularly update the balena CLI to the latest version.
- Device - Device
- [devices](#devices)
- [device <uuid>](#device-uuid)
- [device register <application>](#device-register-application)
- [device rm <uuid>](#device-rm-uuid)
- [device identify <uuid>](#device-identify-uuid)
- [device reboot <uuid>](#device-reboot-uuid)
- [device shutdown <uuid>](#device-shutdown-uuid)
- [device rename <uuid> [newName]](#device-rename-uuid-newname)
- [device move <uuid>](#device-move-uuid)
- [device init](#device-init) - [device init](#device-init)
- [device os-update <uuid>](#device-os-update-uuid) - [device os-update <uuid>](#device-os-update-uuid)
- [device identify <uuid>](#device-identify-uuid)
- [device <uuid>](#device-uuid)
- [device move <uuid>](#device-move-uuid)
- [device reboot <uuid>](#device-reboot-uuid)
- [device register <application>](#device-register-application)
- [device rename <uuid> [newname]](#device-rename-uuid-newname)
- [device rm <uuid>](#device-rm-uuid)
- [device shutdown <uuid>](#device-shutdown-uuid)
- [devices](#devices)
- [devices supported](#devices-supported) - [devices supported](#devices-supported)
- [device public-url <uuid>](#device-public-url-uuid) - [device public-url <uuid>](#device-public-url-uuid)
@ -454,132 +454,6 @@ Examples:
# Device # Device
## devices
Use this command to list all devices that belong to you.
You can filter the devices by application by using the `--application` option.
Examples:
$ balena devices
$ balena devices --application MyApp
$ balena devices --app MyApp
$ balena devices -a MyApp
### Options
#### --application, -a, --app <application>
application name
## device <uuid>
Use this command to show information about a single device.
Examples:
$ balena device 7cf02a6
## device register <application>
Use this command to register a device to an application.
Examples:
$ balena device register MyApp
$ balena device register MyApp --uuid <uuid>
### Options
#### --uuid, -u &#60;uuid&#62;
custom uuid
## device rm &#60;uuid&#62;
Use this command to remove a device from balena.
Notice this command asks for confirmation interactively.
You can avoid this by passing the `--yes` boolean option.
Examples:
$ balena device rm 7cf02a6
$ balena device rm 7cf02a6 --yes
### Options
#### --yes, -y
confirm non interactively
## device identify &#60;uuid&#62;
Use this command to identify a device.
In the Raspberry Pi, the ACT led is blinked several times.
Examples:
$ balena device identify 23c73a1
## device reboot &#60;uuid&#62;
Use this command to remotely reboot a device
Examples:
$ balena device reboot 23c73a1
### Options
#### --force, -f
force action if the update lock is set
## device shutdown &#60;uuid&#62;
Use this command to remotely shutdown a device
Examples:
$ balena device shutdown 23c73a1
### Options
#### --force, -f
force action if the update lock is set
## device rename &#60;uuid&#62; [newName]
Use this command to rename a device.
If you omit the name, you'll get asked for it interactively.
Examples:
$ balena device rename 7cf02a6
$ balena device rename 7cf02a6 MyPi
## device move &#60;uuid&#62;
Use this command to move a device to another application you own.
If you omit the application, you'll get asked for it interactively.
Examples:
$ balena device move 7cf02a6
$ balena device move 7cf02a6 --application MyNewApp
### Options
#### --application, -a, --app &#60;application&#62;
application name
## device init ## device init
Use this command to download the OS image of a certain application and write it to an SD Card. Use this command to download the OS image of a certain application and write it to an SD Card.
@ -646,6 +520,197 @@ a balenaOS version
confirm non interactively confirm non interactively
## device identify &#60;uuid&#62;
Identify a device by making the ACT LED blink (Raspberry Pi).
Examples:
$ balena device identify 23c73a1
### Arguments
#### UUID
the uuid of the device to identify
### Options
## device &#60;uuid&#62;
Show information about a single device.
Examples:
$ balena device 7cf02a6
### Arguments
#### UUID
the device uuid
### Options
## device move &#60;uuid&#62;
Move a device 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 --application MyNewApp
### Arguments
#### UUID
the uuid of the device to move
### Options
#### -a, --application APPLICATION
application name
#### --app APP
same as '--application'
## device reboot &#60;uuid&#62;
Remotely reboot a device.
Examples:
$ balena device reboot 23c73a1
### Arguments
#### UUID
the uuid of the device to reboot
### Options
#### -f, --force
force action if the update lock is set
## device register &#60;application&#62;
Register a device to an application.
Examples:
$ balena device register MyApp
$ balena device register MyApp --uuid <uuid>
### Arguments
#### APPLICATION
the name or id of application to register device with
### Options
#### -u, --uuid UUID
custom uuid
## device rename &#60;uuid&#62; [newName]
Rename a device.
Note, if the name is omitted, it will be prompted for interactively.
Examples:
$ balena device rename 7cf02a6
$ balena device rename 7cf02a6 MyPi
### Arguments
#### UUID
the uuid of the device to rename
#### NEWNAME
the new name for the device
### Options
## device rm &#60;uuid&#62;
Remove a device from balena.
Note this command asks for confirmation interactively.
You can avoid this by passing the `--yes` option.
Examples:
$ balena device rm 7cf02a6
$ balena device rm 7cf02a6 --yes
### Arguments
#### UUID
the uuid of the device to remove
### Options
#### -y, --yes
answer "yes" to all questions (non interactive use)
## device shutdown &#60;uuid&#62;
Remotely shutdown a device.
Examples:
$ balena device shutdown 23c73a1
### Arguments
#### UUID
the uuid of the device to shutdown
### Options
#### -f, --force
force action if the update lock is set
## devices
list all devices that belong to you.
You can filter the devices by application by using the `--application` option.
Examples:
$ balena devices
$ balena devices --application MyApp
$ balena devices --app MyApp
$ balena devices -a MyApp
### Options
#### -a, --application APPLICATION
application name
#### --app APP
same as '--application'
## devices supported ## devices supported
List the supported device types (like 'raspberrypi3' or 'intel-nuc'). List the supported device types (like 'raspberrypi3' or 'intel-nuc').

View File

@ -0,0 +1,75 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceIdentifyCmd extends Command {
public static description = stripIndent`
Identify a device.
Identify a device by making the ACT LED blink (Raspberry Pi).
`;
public static examples = ['$ balena device identify 23c73a1'];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to identify',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
public static usage = 'device identify <uuid>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
const balena = getBalenaSdk();
try {
await balena.models.device.identify(params.uuid);
} catch (e) {
if (e.message === 'Request error: No online device(s) found') {
throw new ExpectedError(`Device ${params.uuid} is not online`);
} else {
throw e;
}
}
}
}

View File

@ -0,0 +1,110 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { Application, Device } from 'balena-sdk';
interface ExtendedDevice extends Device {
dashboard_url?: string;
application_name?: string;
commit?: string;
}
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceCmd extends Command {
public static description = stripIndent`
Show info about a single device.
Show information about a single device.
`;
public static examples = ['$ balena device 7cf02a6'];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the device uuid',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
public static usage = 'device <uuid>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
const balena = getBalenaSdk();
const device: ExtendedDevice = await balena.models.device.get(
params.uuid,
expandForAppName,
);
const deviceStatus = await balena.models.device.getStatus(device.uuid);
device.status = deviceStatus;
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication = device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
device.commit = device.is_on__commit;
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
'id',
'device_type',
'status',
'is_online',
'ip_address',
'application_name',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
]),
);
}
}

View File

@ -0,0 +1,131 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { Application, Device } from 'balena-sdk';
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface ExtendedDevice extends Device {
application_name?: string;
}
interface FlagsDef {
application?: string;
app?: string;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceMoveCmd extends Command {
public static description = stripIndent`
Move a device to another application.
Move a device 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 --application MyNewApp',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to move',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
public static usage = 'device move <uuid>';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
app: cf.app,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceMoveCmd,
);
const balena = getBalenaSdk();
const patterns = await import('../../utils/patterns');
// Consolidate application options
options.application = options.application || options.app;
delete options.app;
const device: ExtendedDevice = await balena.models.device.get(
params.uuid,
expandForAppName,
);
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),
balena.models.config.getDeviceTypes(),
]);
const compatibleDeviceTypes = deviceTypes.filter(
(dt) =>
balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceDeviceType.isDependent &&
dt.state !== 'DISCONTINUED',
);
application = await patterns.selectApplication((app: Application) =>
_.every([
_.some(compatibleDeviceTypes, (dt) => dt.slug === app.device_type),
// @ts-ignore using the extended device object prop
device.application_name !== app.app_name,
]),
);
}
await balena.models.device.move(params.uuid, tryAsInteger(application));
console.info(`${params.uuid} was moved to ${application}`);
}
}

View File

@ -0,0 +1,73 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
force: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceRebootCmd extends Command {
public static description = stripIndent`
Restart a device.
Remotely reboot a device.
`;
public static examples = ['$ balena device reboot 23c73a1'];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to reboot',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
public static usage = 'device reboot <uuid>';
public static flags: flags.Input<FlagsDef> = {
force: cf.force,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceRebootCmd,
);
const balena = getBalenaSdk();
// The SDK current throws "BalenaDeviceNotFound: Device not found: xxxxx"
// when the device is not online, which may be confusing.
// https://github.com/balena-io/balena-cli/issues/1872
await balena.models.device.reboot(params.uuid, options);
}
}

View File

@ -0,0 +1,83 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
uuid?: string;
help: void;
}
interface ArgsDef {
application: string;
}
export default class DeviceRegisterCmd extends Command {
public static description = stripIndent`
Register a device.
Register a device to an application.
`;
public static examples = [
'$ balena device register MyApp',
'$ balena device register MyApp --uuid <uuid>',
];
public static args: Array<IArg<any>> = [
{
name: 'application',
description: 'the name or id of application to register device with',
parse: (app) => tryAsInteger(app),
required: true,
},
];
public static usage = 'device register <application>';
public static flags: flags.Input<FlagsDef> = {
uuid: flags.string({
description: 'custom uuid',
char: 'u',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceRegisterCmd,
);
const balena = getBalenaSdk();
const application = await balena.models.application.get(params.application);
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.app_name}: ${uuid}`);
const result = await balena.models.device.register(application.id, uuid);
return result && result.uuid;
}
}

View File

@ -0,0 +1,85 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
newName?: string;
}
export default class DeviceRenameCmd extends Command {
public static description = stripIndent`
Rename a device.
Rename a device.
Note, if the name is omitted, it will be prompted for interactively.
`;
public static examples = [
'$ balena device rename 7cf02a6',
'$ balena device rename 7cf02a6 MyPi',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to rename',
parse: (dev) => tryAsInteger(dev),
required: true,
},
{
name: 'newName',
description: 'the new name for the device',
},
];
public static usage = 'device rename <uuid> [newName]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceRenameCmd);
const balena = getBalenaSdk();
const form = await import('resin-cli-form');
const newName =
params.newName ||
(await form.ask({
message: 'How do you want to name this device?',
type: 'input',
})) ||
'';
await balena.models.device.rename(params.uuid, newName);
}
}

View File

@ -0,0 +1,84 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceRmCmd extends Command {
public static description = stripIndent`
Remove a device.
Remove a device from balena.
Note this command asks for confirmation interactively.
You can avoid this by passing the \`--yes\` option.
`;
public static examples = [
'$ balena device rm 7cf02a6',
'$ balena device rm 7cf02a6 --yes',
];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to remove',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
public static usage = 'device rm <uuid>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceRmCmd,
);
const balena = getBalenaSdk();
const patterns = await import('../../utils/patterns');
// Confirm
await patterns.confirm(
options.yes,
'Are you sure you want to delete the device?',
);
// Remove
await balena.models.device.remove(params.uuid);
}
}

View File

@ -0,0 +1,79 @@
/**
* @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 { IArg } from '@oclif/parser/lib/args';
import { stripIndent } from 'common-tags';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface FlagsDef {
force: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceShutdownCmd extends Command {
public static description = stripIndent`
Shutdown a device.
Remotely shutdown a device.
`;
public static examples = ['$ balena device shutdown 23c73a1'];
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to shutdown',
parse: (dev) => tryAsInteger(dev),
required: true,
},
];
public static usage = 'device shutdown <uuid>';
public static flags: flags.Input<FlagsDef> = {
force: cf.force,
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceShutdownCmd,
);
const balena = getBalenaSdk();
try {
await balena.models.device.shutdown(params.uuid, options);
} catch (e) {
if (e.message === 'Request error: No online device(s) found') {
throw new ExpectedError(`Device ${params.uuid} is not online`);
} else {
throw e;
}
}
}
}

View File

@ -0,0 +1,113 @@
/**
* @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 { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { Device, Application } from 'balena-sdk';
interface ExtendedDevice extends Device {
dashboard_url?: string;
application_name?: string;
}
interface FlagsDef {
application?: string;
app?: string;
help: void;
}
export default class DevicesCmd extends Command {
public static description = stripIndent`
List all devices.
list all devices that belong to you.
You can filter the devices by application by using the \`--application\` option.
`;
public static examples = [
'$ balena devices',
'$ balena devices --application MyApp',
'$ balena devices --app MyApp',
'$ balena devices -a MyApp',
];
public static usage = 'devices';
public static flags: flags.Input<FlagsDef> = {
application: cf.application,
app: cf.app,
help: cf.help,
};
public static primary = true;
public static authenticated = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
const balena = getBalenaSdk();
// Consolidate application options
options.application = options.application || options.app;
delete options.app;
let devices: ExtendedDevice[];
if (options.application != null) {
devices = await balena.models.device.getAllByApplication(
tryAsInteger(options.application),
expandForAppName,
);
} else {
devices = await balena.models.device.getAll(expandForAppName);
}
devices = _.map(devices, function (device) {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication = device.belongs_to__application as Application[];
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
device.uuid = device.uuid.slice(0, 7);
return device;
});
console.log(
getVisuals().table.horizontal(devices, [
'id',
'uuid',
'device_name',
'device_type',
'application_name',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
]),
);
}
}

View File

@ -17,351 +17,7 @@ limitations under the License.
import * as commandOptions from './command-options'; import * as commandOptions from './command-options';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { normalizeUuidProp } from '../utils/normalization'; import { getBalenaSdk } from '../utils/lazy';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
/** @type {import('balena-sdk').PineOptionsFor<import('balena-sdk').Device>} */
const expandForAppName = {
$expand: { belongs_to__application: { $select: 'app_name' } },
};
export const list = {
signature: 'devices',
description: 'list all devices',
help: `\
Use this command to list all devices that belong to you.
You can filter the devices by application by using the \`--application\` option.
Examples:
$ balena devices
$ balena devices --application MyApp
$ balena devices --app MyApp
$ balena devices -a MyApp\
`,
options: [commandOptions.optionalApplication],
permission: 'user',
primary: true,
action(_params, options) {
const Promise = require('bluebird');
const balena = getBalenaSdk();
return Promise.try(function () {
if (options.application != null) {
return balena.models.device.getAllByApplication(
options.application,
expandForAppName,
);
}
return balena.models.device.getAll(expandForAppName);
}).tap(function (devices) {
devices = _.map(devices, function (device) {
// @ts-ignore extending the device object with extra props
device.dashboard_url = balena.models.device.getDashboardUrl(
device.uuid,
);
// @ts-ignore extending the device object with extra props
device.application_name = device.belongs_to__application?.[0]
? device.belongs_to__application[0].app_name
: 'N/a';
device.uuid = device.uuid.slice(0, 7);
return device;
});
console.log(
getVisuals().table.horizontal(devices, [
'id',
'uuid',
'device_name',
'device_type',
'application_name',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
]),
);
});
},
};
export const info = {
signature: 'device <uuid>',
description: 'list a single device',
help: `\
Use this command to show information about a single device.
Examples:
$ balena device 7cf02a6\
`,
permission: 'user',
primary: true,
action(params) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
return balena.models.device
.get(params.uuid, expandForAppName)
.then((device) =>
// @ts-ignore `device.getStatus` requires a device with service info, but
// this device isn't typed with them, possibly needs fixing?
balena.models.device.getStatus(params.uuid).then(function (status) {
device.status = status;
// @ts-ignore extending the device object with extra props
device.dashboard_url = balena.models.device.getDashboardUrl(
device.uuid,
);
// @ts-ignore extending the device object with extra props
device.application_name = device.belongs_to__application?.[0]
? device.belongs_to__application[0].app_name
: 'N/a';
// @ts-ignore extending the device object with extra props
device.commit = device.is_on__commit;
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
'id',
'device_type',
'status',
'is_online',
'ip_address',
'mac_address',
'application_name',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
]),
);
}),
);
},
};
export const register = {
signature: 'device register <application>',
description: 'register a device',
help: `\
Use this command to register a device to an application.
Examples:
$ balena device register MyApp
$ balena device register MyApp --uuid <uuid>\
`,
permission: 'user',
options: [
{
signature: 'uuid',
description: 'custom uuid',
parameter: 'uuid',
alias: 'u',
},
],
action(params, options) {
const Promise = require('bluebird');
const balena = getBalenaSdk();
return Promise.join(
balena.models.application.get(params.application),
options.uuid ?? balena.models.device.generateUniqueKey(),
function (application, uuid) {
console.info(`Registering to ${application.app_name}: ${uuid}`);
return balena.models.device.register(application.id, uuid);
},
).get('uuid');
},
};
export const remove = {
signature: 'device rm <uuid>',
description: 'remove a device',
help: `\
Use this command to remove a device from balena.
Notice this command asks for confirmation interactively.
You can avoid this by passing the \`--yes\` boolean option.
Examples:
$ balena device rm 7cf02a6
$ balena device rm 7cf02a6 --yes\
`,
options: [commandOptions.yes],
permission: 'user',
action(params, options) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
const patterns = require('../utils/patterns');
return patterns
.confirm(options.yes, 'Are you sure you want to delete the device?')
.then(() => balena.models.device.remove(params.uuid));
},
};
export const identify = {
signature: 'device identify <uuid>',
description: 'identify a device with a UUID',
help: `\
Use this command to identify a device.
In the Raspberry Pi, the ACT led is blinked several times.
Examples:
$ balena device identify 23c73a1\
`,
permission: 'user',
action(params) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
return balena.models.device.identify(params.uuid);
},
};
export const reboot = {
signature: 'device reboot <uuid>',
description: 'restart a device',
help: `\
Use this command to remotely reboot a device
Examples:
$ balena device reboot 23c73a1\
`,
options: [commandOptions.forceUpdateLock],
permission: 'user',
action(params, options) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
return balena.models.device.reboot(params.uuid, options);
},
};
export const shutdown = {
signature: 'device shutdown <uuid>',
description: 'shutdown a device',
help: `\
Use this command to remotely shutdown a device
Examples:
$ balena device shutdown 23c73a1\
`,
options: [commandOptions.forceUpdateLock],
permission: 'user',
action(params, options) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
return balena.models.device.shutdown(params.uuid, options);
},
};
export const rename = {
signature: 'device rename <uuid> [newName]',
description: 'rename a balena device',
help: `\
Use this command to rename a device.
If you omit the name, you'll get asked for it interactively.
Examples:
$ balena device rename 7cf02a6
$ balena device rename 7cf02a6 MyPi\
`,
permission: 'user',
action(params) {
normalizeUuidProp(params);
const Promise = require('bluebird');
const balena = getBalenaSdk();
const form = require('resin-cli-form');
return Promise.try(function () {
if (!_.isEmpty(params.newName)) {
return params.newName;
}
return form.ask({
message: 'How do you want to name this device?',
type: 'input',
});
}).then(_.partial(balena.models.device.rename, params.uuid));
},
};
export const move = {
signature: 'device move <uuid>',
description: 'move a device to another application',
help: `\
Use this command to move a device to another application you own.
If you omit the application, you'll get asked for it interactively.
Examples:
$ balena device move 7cf02a6
$ balena device move 7cf02a6 --application MyNewApp\
`,
permission: 'user',
options: [commandOptions.optionalApplication],
action(params, options) {
normalizeUuidProp(params);
const balena = getBalenaSdk();
const patterns = require('../utils/patterns');
return balena.models.device
.get(params.uuid, expandForAppName)
.then(function (device) {
// @ts-ignore extending the device object with extra props
device.application_name = device.belongs_to__application?.[0]
? device.belongs_to__application[0].app_name
: 'N/a';
if (options.application) {
return options.application;
}
return Promise.all([
balena.models.device.getManifestBySlug(device.device_type),
balena.models.config.getDeviceTypes(),
]).then(function ([deviceDeviceType, deviceTypes]) {
const compatibleDeviceTypes = deviceTypes.filter(
(dt) =>
balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch,
dt.arch,
) &&
!!dt.isDependent === !!deviceDeviceType.isDependent &&
dt.state !== 'DISCONTINUED',
);
return patterns.selectApplication((application) =>
_.every([
_.some(
compatibleDeviceTypes,
(dt) => dt.slug === application.device_type,
),
// @ts-ignore using the extended device object prop
device.application_name !== application.app_name,
]),
);
});
})
.tap((application) => balena.models.device.move(params.uuid, application))
.then((application) => {
console.info(`${params.uuid} was moved to ${application}`);
});
},
};
export const init = { export const init = {
signature: 'device init', signature: 'device init',

View File

@ -53,17 +53,8 @@ capitano.command(actions.auth.logout);
capitano.command(actions.auth.whoami); capitano.command(actions.auth.whoami);
// ---------- Device Module ---------- // ---------- Device Module ----------
capitano.command(actions.device.list);
capitano.command(actions.device.rename);
capitano.command(actions.device.init); capitano.command(actions.device.init);
capitano.command(actions.device.remove);
capitano.command(actions.device.identify);
capitano.command(actions.device.reboot);
capitano.command(actions.device.shutdown);
capitano.command(actions.device.register);
capitano.command(actions.device.move);
capitano.command(actions.device.osUpdate); capitano.command(actions.device.osUpdate);
capitano.command(actions.device.info);
// ---------- OS Module ---------- // ---------- OS Module ----------
capitano.command(actions.os.versions); capitano.command(actions.os.versions);

View File

@ -139,7 +139,8 @@ const EXPECTED_ERROR_REGEXES = [
/^BalenaApplicationNotFound/, // balena-sdk /^BalenaApplicationNotFound/, // balena-sdk
/^BalenaDeviceNotFound/, // balena-sdk /^BalenaDeviceNotFound/, // balena-sdk
/^BalenaExpiredToken/, // balena-sdk /^BalenaExpiredToken/, // balena-sdk
/^Missing \w+$/, // Capitano, oclif parser: RequiredArgsError, RequiredFlagError /^Missing \w+$/, // Capitano,
/^Missing \d required argument/, // oclif parser: RequiredArgsError, RequiredFlagError
/^Unexpected argument/, // oclif parser: UnexpectedArgsError /^Unexpected argument/, // oclif parser: UnexpectedArgsError
/to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError /to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError
]; ];

View File

@ -141,7 +141,16 @@ export const convertedCommands = [
'app:rm', 'app:rm',
'apps', 'apps',
'api-key:generate', 'api-key:generate',
'device',
'device:identify',
'device:move',
'device:public-url', 'device:public-url',
'device:reboot',
'device:register',
'device:rename',
'device:rm',
'device:shutdown',
'devices',
'devices:supported', 'devices:supported',
'envs', 'envs',
'env:add', 'env:add',

View File

@ -23,6 +23,10 @@ export const application = flags.string({
char: 'a', char: 'a',
description: 'application name', description: 'application name',
}); });
// TODO: Consider remove second alias 'app' when we can, to simplify.
export const app = flags.string({
description: "same as '--application'",
});
export const device = flags.string({ export const device = flags.string({
char: 'd', char: 'd',
@ -56,3 +60,8 @@ export const yes: IBooleanFlag<boolean> = flags.boolean({
char: 'y', char: 'y',
description: 'answer "yes" to all questions (non interactive use)', description: 'answer "yes" to all questions (non interactive use)',
}); });
export const force: IBooleanFlag<boolean> = flags.boolean({
char: 'f',
description: 'force action if the update lock is set',
});

View File

@ -22,6 +22,7 @@ import * as _ from 'lodash';
import * as os from 'os'; import * as os from 'os';
import * as ShellEscape from 'shell-escape'; import * as ShellEscape from 'shell-escape';
import { Device, PineOptionsFor } from 'balena-sdk';
import { ExpectedError } from '../errors'; import { ExpectedError } from '../errors';
import { getBalenaSdk, getChalk, getVisuals } from './lazy'; import { getBalenaSdk, getChalk, getVisuals } from './lazy';
@ -466,3 +467,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
} }
} }
} }
export const expandForAppName: PineOptionsFor<Device> = {
$expand: { belongs_to__application: { $select: 'app_name' } },
};

View File

@ -20,20 +20,28 @@ import { BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
const HELP_RESPONSE = ` const HELP_RESPONSE = `
Usage: device move <uuid> Move a device to another application.
Use this command to move a device to another application you own. USAGE
$ balena device move <uuid>
If you omit the application, you'll get asked for it interactively. ARGUMENTS
<uuid> the uuid of the device to move
Examples: OPTIONS
-a, --application <application> application name
-h, --help show CLI help
--app <app> same as '--application'
\t$ balena device move 7cf02a6 DESCRIPTION
\t$ balena device move 7cf02a6 --application MyNewApp Move a device to another application.
Options: Note, if the application option is omitted it will be prompted
for interactively.
--application, -a, --app <application> application name EXAMPLES
$ balena device move 7cf02a6
$ balena device move 7cf02a6 --application MyNewApp
`; `;
describe('balena device move', function () { describe('balena device move', function () {
@ -49,7 +57,7 @@ describe('balena device move', function () {
}); });
it('should print help text with the -h flag', async () => { it('should print help text with the -h flag', async () => {
api.expectGetWhoAmI({ optional: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('device move -h'); const { out, err } = await runCommand('device move -h');
@ -59,16 +67,15 @@ describe('balena device move', function () {
expect(err).to.eql([]); expect(err).to.eql([]);
}); });
it.skip('should error if uuid not provided', async () => { it('should error if uuid not provided', async () => {
// TODO: Figure out how to test for expected errors with current setup api.expectGetWhoAmI({ optional: true, persist: true });
// including exit codes if possible.
api.expectGetWhoAmI({ optional: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('device move'); const { out, err } = await runCommand('device move');
const errLines = cleanOutput(err); const errLines = cleanOutput(err);
expect(errLines[0]).to.equal('Missing uuid'); expect(errLines[0]).to.equal('Missing 1 required argument:');
expect(errLines[1]).to.equal('uuid : the uuid of the device to move');
expect(out).to.eql([]); expect(out).to.eql([]);
}); });

View File

@ -22,13 +22,22 @@ import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
const HELP_RESPONSE = ` const HELP_RESPONSE = `
Usage: device <uuid> Show info about a single device.
Use this command to show information about a single device. USAGE
$ balena device <uuid>
Examples: ARGUMENTS
<uuid> the device uuid
\t$ balena device 7cf02a6 OPTIONS
-h, --help show CLI help
DESCRIPTION
Show information about a single device.
EXAMPLE
$ balena device 7cf02a6
`; `;
describe('balena device', function () { describe('balena device', function () {
@ -44,7 +53,7 @@ describe('balena device', function () {
}); });
it('should print help text with the -h flag', async () => { it('should print help text with the -h flag', async () => {
api.expectGetWhoAmI({ optional: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('device -h'); const { out, err } = await runCommand('device -h');
@ -54,16 +63,15 @@ describe('balena device', function () {
expect(err).to.eql([]); expect(err).to.eql([]);
}); });
it.skip('should error if uuid not provided', async () => { it('should error if uuid not provided', async () => {
// TODO: Figure out how to test for expected errors with current setup api.expectGetWhoAmI({ optional: true, persist: true });
// including exit codes if possible.
api.expectGetWhoAmI({ optional: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('device'); const { out, err } = await runCommand('device');
const errLines = cleanOutput(err); const errLines = cleanOutput(err);
expect(errLines[0]).to.equal('Missing uuid'); expect(errLines[0]).to.equal('Missing 1 required argument:');
expect(errLines[1]).to.equal('uuid : the device uuid');
expect(out).to.eql([]); expect(out).to.eql([]);
}); });

View File

@ -22,22 +22,26 @@ import { apiResponsePath, BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
const HELP_RESPONSE = ` const HELP_RESPONSE = `
Usage: devices List all devices.
Use this command to list all devices that belong to you. USAGE
$ balena devices
OPTIONS
-a, --application <application> application name
-h, --help show CLI help
--app <app> same as '--application'
DESCRIPTION
list all devices that belong to you.
You can filter the devices by application by using the \`--application\` option. You can filter the devices by application by using the \`--application\` option.
Examples: EXAMPLES
$ balena devices
\t$ balena devices $ balena devices --application MyApp
\t$ balena devices --application MyApp $ balena devices --app MyApp
\t$ balena devices --app MyApp $ balena devices -a MyApp
\t$ balena devices -a MyApp
Options:
--application, -a, --app <application> application name
`; `;
describe('balena devices', function () { describe('balena devices', function () {
@ -53,7 +57,7 @@ describe('balena devices', function () {
}); });
it('should print help text with the -h flag', async () => { it('should print help text with the -h flag', async () => {
api.expectGetWhoAmI({ optional: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
const { out, err } = await runCommand('devices -h'); const { out, err } = await runCommand('devices -h');

View File

@ -14,7 +14,7 @@ Primary commands:
apps list all applications apps list all applications
app <name> display information about a single application app <name> display information about a single application
devices list all devices devices list all devices
device <uuid> list a single device device <uuid> show info about a single device
tunnel <deviceOrApplication> Tunnel local ports to your balenaOS device tunnel <deviceOrApplication> Tunnel local ports to your balenaOS device
preload <image> preload an app on a disk image (or Edison zip archive) preload <image> preload an app on a disk image (or Edison zip archive)
build [source] Build a single image or a multicontainer project locally build [source] Build a single image or a multicontainer project locally
@ -37,14 +37,14 @@ Additional commands:
config read read a device configuration config read read a device configuration
config reconfigure reconfigure a provisioned device config reconfigure reconfigure a provisioned device
config write <key> <value> write a device configuration config write <key> <value> write a device configuration
device identify <uuid> identify a device with a UUID device identify <uuid> identify a device
device init initialise a device with balenaOS device init initialise a device with balenaOS
device move <uuid> move a device to another application device move <uuid> move a device to another application
device os-update <uuid> Start a Host OS update for a device 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 public-url <uuid> get or manage the public URL for a device
device reboot <uuid> restart a device device reboot <uuid> restart a device
device register <application> register a device device register <application> register a device
device rename <uuid> [newName] rename a balena device device rename <uuid> [newname] rename a device
device rm <uuid> remove a device device rm <uuid> remove a device
device shutdown <uuid> shutdown a device device shutdown <uuid> shutdown a device
devices supported list the supported device types (like 'raspberrypi3' or 'intel-nuc') devices supported list the supported device types (like 'raspberrypi3' or 'intel-nuc')

View File

@ -117,8 +117,9 @@ describe('handleError() function', () => {
}); });
const messagesToMatch = [ const messagesToMatch = [
'Missing argument', 'Missing uuid', // Capitano
'Missing arguments', 'Missing 1 required argument', // oclif
'Missing 2 required arguments', // oclif
'Unexpected argument', 'Unexpected argument',
'Unexpected arguments', 'Unexpected arguments',
'to be one of', 'to be one of',