device os-update: allow host OS upgrade with development balenaOS images

also:
fix `device os-update` incorrectly showing 0% progress
convert `device os-update` to use async/await

Change-type: minor
Resolves: #1725
Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
Scott Lowe 2020-04-15 12:39:27 +02:00
parent 7f79451376
commit 320b4864d9
6 changed files with 105 additions and 83 deletions

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2016-2017 Balena Copyright 2016-2020 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@ limitations under the License.
import { Device } from 'balena-sdk'; import { Device } from 'balena-sdk';
import { CommandDefinition } from 'capitano'; import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags'; import { stripIndent } from 'common-tags';
import { ExpectedError } from '../errors';
import { getBalenaSdk } from '../utils/lazy'; import { getBalenaSdk } from '../utils/lazy';
import { normalizeUuidProp } from '../utils/normalization'; import { normalizeUuidProp } from '../utils/normalization';
import * as commandOptions from './command-options'; import * as commandOptions from './command-options';
@ -55,62 +56,67 @@ export const osUpdate: CommandDefinition<OsUpdate.Args, OsUpdate.Options> = {
const patterns = await import('../utils/patterns'); const patterns = await import('../utils/patterns');
const form = await import('resin-cli-form'); const form = await import('resin-cli-form');
return sdk.models.device // Get device info
.get(params.uuid, { const {
$select: ['uuid', 'device_type', 'os_version', 'os_variant'], uuid,
}) device_type,
.then(async ({ uuid, device_type, os_version, os_variant }) => { os_version,
const currentOsVersion = sdk.models.device.getOsVersion({ os_variant,
os_version, } = await sdk.models.device.get(params.uuid, {
os_variant, $select: ['uuid', 'device_type', 'os_version', 'os_variant'],
} as Device); });
if (!currentOsVersion) {
patterns.exitWithExpectedError(
'The current os version of the device is not available',
);
// Just to make TS happy
return;
}
return sdk.models.os // Get current device OS version
.getSupportedOsUpdateVersions(device_type, currentOsVersion) const currentOsVersion = sdk.models.device.getOsVersion({
.then(hupVersionInfo => { os_version,
if (hupVersionInfo.versions.length === 0) { os_variant,
patterns.exitWithExpectedError( } as Device);
'There are no available Host OS update targets for this device', if (!currentOsVersion) {
); throw new ExpectedError(
} 'The current os version of the device is not available',
);
}
if (options.version != null) { // Get supported OS update versions
if (!_.includes(hupVersionInfo.versions, options.version)) { const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
patterns.exitWithExpectedError( device_type,
'The provided version is not in the Host OS update targets for this device', currentOsVersion,
); );
} if (hupVersionInfo.versions.length === 0) {
return options.version; throw new ExpectedError(
} 'There are no available Host OS update targets for this device',
);
}
return form.ask({ // Get target OS version
message: 'Target OS version', let targetOsVersion = options.version;
type: 'list', if (targetOsVersion != null) {
choices: hupVersionInfo.versions.map(version => ({ if (!_.includes(hupVersionInfo.versions, targetOsVersion)) {
name: throw new ExpectedError(
hupVersionInfo.recommended === version `The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
? `${version} (recommended)` );
: version, }
value: version, } else {
})), targetOsVersion = await form.ask({
}); message: 'Target OS version',
}) type: 'list',
.then(version => choices: hupVersionInfo.versions.map(version => ({
patterns name:
.confirm( hupVersionInfo.recommended === version
options.yes || false, ? `${version} (recommended)`
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?', : version,
) value: version,
.then(() => sdk.models.device.startOsUpdate(uuid, version)) })),
.then(() => patterns.awaitDeviceOsUpdate(uuid, version)),
);
}); });
}
// Confirm and start update
await patterns.confirm(
options.yes || false,
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
);
await sdk.models.device.startOsUpdate(uuid, targetOsVersion);
await patterns.awaitDeviceOsUpdate(uuid, targetOsVersion);
}, },
}; };

View File

@ -48,7 +48,7 @@ Examples:
`, `,
permission: 'user', permission: 'user',
async action(params) { async action(params) {
const key = await getBalenaSdk().models.key.get(params.id); const key = await getBalenaSdk().models.key.get(parseInt(params.id, 10));
console.log(getVisuals().table.vertical(key, ['id', 'title'])); console.log(getVisuals().table.vertical(key, ['id', 'title']));
@ -86,7 +86,7 @@ Examples:
'Are you sure you want to delete the key?', 'Are you sure you want to delete the key?',
); );
await getBalenaSdk().models.key.remove(params.id); await getBalenaSdk().models.key.remove(parseInt(params.id, 10));
}, },
}; };

View File

@ -18,7 +18,7 @@ import { Device } from 'balena-sdk';
export const getDeviceOsProgress = (device: Device) => { export const getDeviceOsProgress = (device: Device) => {
if (!device.is_online) { if (!device.is_online) {
return 0; return null;
} }
const status = BalenaDeviceStatus.getStatus(device).key; const status = BalenaDeviceStatus.getStatus(device).key;

View File

@ -293,10 +293,7 @@ export function awaitDeviceOsUpdate(uuid: string, targetOsVersion: string) {
balena.models.device.getOsUpdateStatus(uuid), balena.models.device.getOsUpdateStatus(uuid),
balena.models.device.get(uuid).then(getDeviceOsProgress), balena.models.device.get(uuid).then(getDeviceOsProgress),
]).then(([osUpdateStatus, osUpdateProgress]) => { ]).then(([osUpdateStatus, osUpdateProgress]) => {
if ( if (osUpdateStatus.status === 'done') {
osUpdateStatus.status === 'update_done' ||
osUpdateStatus.status === 'done'
) {
console.info( console.info(
`The device ${deviceName} has been updated to v${targetOsVersion} and will restart shortly!`, `The device ${deviceName} has been updated to v${targetOsVersion} and will restart shortly!`,
); );
@ -311,7 +308,10 @@ export function awaitDeviceOsUpdate(uuid: string, targetOsVersion: string) {
return; return;
} }
progressBar.update({ percentage: osUpdateProgress }); if (osUpdateProgress !== null) {
// Avoid resetting to 0% at end of process when device goes offline.
progressBar.update({ percentage: osUpdateProgress });
}
return Bluebird.delay(3000).then(poll); return Bluebird.delay(3000).then(poll);
}); });
@ -451,6 +451,8 @@ export function printErrorMessage(message: string) {
* "expected" errors (say, a JSON parsing error in a file provided by the user) * "expected" errors (say, a JSON parsing error in a file provided by the user)
* don't warrant reporting through Sentry.io. For such mundane errors, catch * don't warrant reporting through Sentry.io. For such mundane errors, catch
* them and call this function. * them and call this function.
*
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
*/ */
export function exitWithExpectedError(message: string | Error): never { export function exitWithExpectedError(message: string | Error): never {
if (message instanceof Error) { if (message instanceof Error) {

54
npm-shrinkwrap.json generated
View File

@ -2089,9 +2089,9 @@
} }
}, },
"balena-hup-action-utils": { "balena-hup-action-utils": {
"version": "3.0.2", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-3.0.2.tgz", "resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.0.0.tgz",
"integrity": "sha512-AwTAFQN4mtjsRLhkzspMKGBykWKvjVlZnzhAu6uyPBn8PGocyB3D30bR2jWgapd6uSdPJf38fcspRDbcpx3CHA==", "integrity": "sha512-Qwk4h6QRqBiJNONRyoHPKtxgoRucdCK5VTG0A6LSCAo0qQrBRi4Alhdq76ksHQgZ2t/Gewfa7cuVAgiH78YMfw==",
"requires": { "requires": {
"balena-semver": "^2.0.0", "balena-semver": "^2.0.0",
"lodash": "^4.17.11" "lodash": "^4.17.11"
@ -2124,9 +2124,9 @@
} }
}, },
"balena-pine": { "balena-pine": {
"version": "10.1.1", "version": "10.1.3",
"resolved": "https://registry.npmjs.org/balena-pine/-/balena-pine-10.1.1.tgz", "resolved": "https://registry.npmjs.org/balena-pine/-/balena-pine-10.1.3.tgz",
"integrity": "sha512-Py0Id0w1QwwM20/gmZ4hRgTOf0sbCwmjGwkE0G/AO6fqnlFDe5ehaebzXk3AtWmW3K7WGDPUxcNAHohHG8aRJA==", "integrity": "sha512-dOjftYx7vf9e3C9ENJyYyD8DNtQl6CxXuWgvNImfvK3Rw+zSRBPYfLw6S4MqbCWkcsQGjaS/zun/VJ9UNjJYhQ==",
"requires": { "requires": {
"balena-errors": "^4.2.1", "balena-errors": "^4.2.1",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
@ -2235,16 +2235,16 @@
}, },
"dependencies": { "dependencies": {
"qs": { "qs": {
"version": "6.9.1", "version": "6.9.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
"integrity": "sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==" "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw=="
} }
} }
}, },
"balena-sdk": { "balena-sdk": {
"version": "12.29.1", "version": "12.33.0",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-12.29.1.tgz", "resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-12.33.0.tgz",
"integrity": "sha512-e6nIuP16AaDpGQnOCDPR7kptCM7/iWHPp6bdmUeBsw9e5IX4zwI0FwFHi4L9EoRxByLKI5oRZYeYGeU++Sd1Nw==", "integrity": "sha512-riqcJeA8SYMf20bgt1lhgw/eWiWXVwivGHgBer54/dxDIo48RIwys98LvkisIO2sTco1AmTL4Q0rePsMGjEYwQ==",
"requires": { "requires": {
"@types/bluebird": "^3.5.30", "@types/bluebird": "^3.5.30",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.149",
@ -2253,8 +2253,8 @@
"abortcontroller-polyfill": "^1.4.0", "abortcontroller-polyfill": "^1.4.0",
"balena-auth": "^3.0.1", "balena-auth": "^3.0.1",
"balena-device-status": "^3.2.1", "balena-device-status": "^3.2.1",
"balena-errors": "^4.2.1", "balena-errors": "^4.3.0",
"balena-hup-action-utils": "~3.0.0", "balena-hup-action-utils": "~4.0.0",
"balena-pine": "^10.1.1", "balena-pine": "^10.1.1",
"balena-register-device": "^6.0.1", "balena-register-device": "^6.0.1",
"balena-request": "^10.0.8", "balena-request": "^10.0.8",
@ -2269,9 +2269,23 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "8.10.59", "version": "8.10.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.60.tgz",
"integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" "integrity": "sha512-YjPbypHFuiOV0bTgeF07HpEEqhmHaZqYNSdCKeBJa+yFoQ/7BC+FpJcwmi34xUIIRVFktnUyP1dPU8U0612GOg=="
},
"balena-errors": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/balena-errors/-/balena-errors-4.3.2.tgz",
"integrity": "sha512-daa+3jHqvoha8zIIIX4jvxdus1LWhH+2Y5IJOzOuIzxmfLEq51EXeszMbcHrbSLlGlJpfaJDG7dfBmFXtabL6Q==",
"requires": {
"tslib": "^1.11.1",
"typed-error": "^3.0.0"
}
},
"tslib": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
} }
} }
}, },
@ -2314,9 +2328,9 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "8.10.59", "version": "8.10.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.60.tgz",
"integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" "integrity": "sha512-YjPbypHFuiOV0bTgeF07HpEEqhmHaZqYNSdCKeBJa+yFoQ/7BC+FpJcwmi34xUIIRVFktnUyP1dPU8U0612GOg=="
} }
} }
}, },

View File

@ -173,7 +173,7 @@
"balena-errors": "^4.2.1", "balena-errors": "^4.2.1",
"balena-image-manager": "^6.1.2", "balena-image-manager": "^6.1.2",
"balena-preload": "^8.4.0", "balena-preload": "^8.4.0",
"balena-sdk": "^12.29.1", "balena-sdk": "^12.33.0",
"balena-semver": "^2.2.0", "balena-semver": "^2.2.0",
"balena-settings-client": "^4.0.4", "balena-settings-client": "^4.0.4",
"balena-sync": "^10.2.0", "balena-sync": "^10.2.0",