From 4d42f74c0ce176b27699f260c13906b1c3830e93 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Mon, 15 Oct 2018 19:03:21 +0200 Subject: [PATCH] Add support for the Opensource provisioning flow Connects-to: #978 Change-type: major Depends-on: https://github.com/resin-io/resin-sdk/pull/594 HQ: https://github.com/resin-io/balena/pull/1140 Signed-off-by: Thodoris Greasidis --- doc/cli.markdown | 10 ++- lib/actions/command-options.coffee | 6 +- lib/actions/config.coffee | 5 +- lib/actions/device.coffee | 2 +- lib/actions/os.coffee | 24 ++----- lib/utils/config.ts | 101 +++++++++++++---------------- lib/utils/promote.ts | 28 +++++++- 7 files changed, 87 insertions(+), 89 deletions(-) diff --git a/doc/cli.markdown b/doc/cli.markdown index c7e0b96f..91808f52 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -142,7 +142,7 @@ environment variable (in the same standard URL format). - [os versions <type>](#os-versions-type-) - [os download <type>](#os-download-type-) - [os build-config <image> <device-type>](#os-build-config-image-device-type-) - - [os configure <image> [uuid] [deviceApiKey]](#os-configure-image-uuid-deviceapikey-) + - [os configure <image>](#os-configure-image-) - [os initialize <image>](#os-initialize-image-) - Config @@ -965,13 +965,12 @@ show advanced configuration options the path to the output JSON file -## os configure <image> [uuid] [deviceApiKey] +## os configure <image> Use this command to configure a previously downloaded operating system image for the specific device or for an application generally. -Calling this command without --version is not recommended, and may fail in -future releases if the OS version cannot be inferred. +Calling this command with the exact version number of the targeted image is required. Note that device api keys are only supported on ResinOS 2.0.3+. @@ -1125,8 +1124,7 @@ show advanced commands Use this command to generate a config.json for a device or application. -Calling this command without --version is not recommended, and may fail in -future releases if the OS version cannot be inferred. +Calling this command with the exact version number of the targeted image is required. This is interactive by default, but you can do this automatically without interactivity by specifying an option for each question on the command line, if you know the questions diff --git a/lib/actions/command-options.coffee b/lib/actions/command-options.coffee index 5ff16989..a5a98cb4 100644 --- a/lib/actions/command-options.coffee +++ b/lib/actions/command-options.coffee @@ -49,13 +49,17 @@ exports.optionalOsVersion = description: 'a resinOS version' parameter: 'version' +exports.osVersion = _.defaults + required: 'You have to specify an exact os version' +, exports.optionalOsVersion + exports.booleanDevice = signature: 'device' description: 'device' boolean: true alias: 'd' -exports.osVersion = +exports.osVersionOrSemver = signature: 'version' description: """ exact version number, or a valid semver range, diff --git a/lib/actions/config.coffee b/lib/actions/config.coffee index 32b9a127..7b039619 100644 --- a/lib/actions/config.coffee +++ b/lib/actions/config.coffee @@ -223,8 +223,7 @@ exports.generate = help: ''' Use this command to generate a config.json for a device or application. - Calling this command without --version is not recommended, and may fail in - future releases if the OS version cannot be inferred. + Calling this command with the exact version number of the targeted image is required. This is interactive by default, but you can do this automatically without interactivity by specifying an option for each question on the command line, if you know the questions @@ -242,7 +241,7 @@ exports.generate = --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1 ''' options: [ - commandOptions.optionalOsVersion + commandOptions.osVersion commandOptions.optionalApplication commandOptions.optionalDevice commandOptions.optionalDeviceApiKey diff --git a/lib/actions/device.coffee b/lib/actions/device.coffee index e052b623..278580a9 100644 --- a/lib/actions/device.coffee +++ b/lib/actions/device.coffee @@ -395,7 +395,7 @@ exports.init = commandOptions.optionalApplication commandOptions.yes commandOptions.advancedConfig - _.assign({}, commandOptions.osVersion, { signature: 'os-version', parameter: 'os-version' }) + _.assign({}, commandOptions.osVersionOrSemver, { signature: 'os-version', parameter: 'os-version' }) commandOptions.drive { signature: 'config' diff --git a/lib/actions/os.coffee b/lib/actions/os.coffee index 1adb2eb5..4155415d 100644 --- a/lib/actions/os.coffee +++ b/lib/actions/os.coffee @@ -96,7 +96,7 @@ exports.download = alias: 'o' required: 'You have to specify the output location' } - commandOptions.osVersion + commandOptions.osVersionOrSemver ] action: (params, options, done) -> Promise = require('bluebird') @@ -197,14 +197,13 @@ exports.buildConfig = .nodeify(done) exports.configure = - signature: 'os configure [uuid] [deviceApiKey]' + signature: 'os configure ' description: 'configure an os image' help: ''' Use this command to configure a previously downloaded operating system image for the specific device or for an application generally. - Calling this command without --version is not recommended, and may fail in - future releases if the OS version cannot be inferred. + Calling this command with the exact version number of the targeted image is required. Note that device api keys are only supported on ResinOS 2.0.3+. @@ -224,7 +223,7 @@ exports.configure = commandOptions.optionalApplication commandOptions.optionalDevice commandOptions.optionalDeviceApiKey - commandOptions.optionalOsVersion + commandOptions.osVersion { signature: 'config' description: 'path to the config JSON file, see `resin os build-config`' @@ -232,7 +231,6 @@ exports.configure = } ] action: (params, options, done) -> - normalizeUuidProp(params) normalizeUuidProp(options, 'device') fs = require('fs') Promise = require('bluebird') @@ -246,30 +244,20 @@ exports.configure = if _.filter([ options.device options.application - params.uuid ]).length != 1 patterns.exitWithExpectedError ''' To configure an image, you must provide exactly one of: * A device, with --device * An application, with --app - * [Deprecated] A device, passing its uuid directly on the command line See the help page for examples: $ resin help os configure ''' - if params.uuid - console.warn( - 'Directly passing a UUID to `resin os configure` is deprecated, and will stop working in future.\n' + - 'Pass your device UUID with --device instead.' + - if params.deviceApiKey - ' Device api keys can be passed with --deviceApiKey.\n' - else '\n' - ) - uuid = options.device || params.uuid - deviceApiKey = options.deviceApiKey || params.deviceApiKey + uuid = options.device + deviceApiKey = options.deviceApiKey console.info('Configuring operating system image') diff --git a/lib/utils/config.ts b/lib/utils/config.ts index 37d883ef..4be50b02 100644 --- a/lib/utils/config.ts +++ b/lib/utils/config.ts @@ -13,77 +13,64 @@ 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 * as fs from 'fs'; - import Promise = require('bluebird'); import ResinSdk = require('resin-sdk'); -import deviceConfig = require('resin-device-config'); import * as semver from 'resin-semver'; const resin = ResinSdk.fromSharedOptions(); -function readRootCa(): Promise { - const caFile = process.env.NODE_EXTRA_CA_CERTS; - if (!caFile) { - return Promise.resolve(); - } - return Promise.fromCallback(cb => - fs.readFile(caFile, { encoding: 'utf8' }, cb), - ) - .then(pem => Buffer.from(pem).toString('base64')) - .catch({ code: 'ENOENT' }, () => {}); -} +type ImgConfig = { + applicationName: string; + applicationId: number; + deviceType: string; + userId: number; + username: string; + appUpdatePollInterval: number; + listenPort: number; + vpnPort: number; + apiEndpoint: string; + vpnEndpoint: string; + registryEndpoint: string; + deltaEndpoint: string; + pubnubSubscribeKey: string; + pubnubPublishKey: string; + mixpanelToken: string; + wifiSsid?: string; + wifiKey?: string; + + // props for older OS versions + connectivity?: string; + files?: { + [filepath: string]: string; + }; + + // device specific config props + deviceId?: number; + uuid?: string; + registered_at?: number; +}; export function generateBaseConfig( application: ResinSdk.Application, - options: { version?: string; appUpdatePollInterval?: number }, -) { - if (options.appUpdatePollInterval) { - options = { - ...options, - appUpdatePollInterval: options.appUpdatePollInterval * 60 * 1000, - }; - } + options: { version: string; appUpdatePollInterval?: number }, +): Promise { + options = { + ...options, + appUpdatePollInterval: options.appUpdatePollInterval || 10, + }; - return Promise.props({ - userId: resin.auth.getUserId(), - username: resin.auth.whoami(), - apiUrl: resin.settings.get('apiUrl'), - vpnUrl: resin.settings.get('vpnUrl'), - registryUrl: resin.settings.get('registryUrl'), - deltaUrl: resin.settings.get('deltaUrl'), - apiConfig: resin.models.config.getAll(), - rootCA: readRootCa().catch(() => { - console.warn('Could not read root CA'); - }), - }).then(results => { - return deviceConfig.generate( - { - application, - user: { - id: results.userId, - username: results.username, - }, - endpoints: { - api: results.apiUrl, - vpn: results.vpnUrl, - registry: results.registryUrl, - delta: results.deltaUrl, - }, - pubnub: results.apiConfig.pubnub, - mixpanel: { - token: results.apiConfig.mixpanelToken, - }, - balenaRootCA: results.rootCA, - }, - options, - ); + const promise = resin.models.os.getConfig(application.app_name, options) as Promise< + ImgConfig & { apiKey?: string; } + >; + return promise.tap(config => { + // os.getConfig always returns a config for an app + delete config.apiKey; }); } export function generateApplicationConfig( application: ResinSdk.Application, - options: { version?: string }, + options: { version: string }, ) { return generateBaseConfig(application, options).tap(config => { if (semver.satisfies(options.version, '>=2.7.8')) { @@ -97,7 +84,7 @@ export function generateApplicationConfig( export function generateDeviceConfig( device: ResinSdk.Device & { belongs_to__application: ResinSdk.PineDeferred }, deviceApiKey: string | true | null, - options: { version?: string }, + options: { version: string }, ) { return resin.models.application .get(device.belongs_to__application.__id) diff --git a/lib/utils/promote.ts b/lib/utils/promote.ts index 5c81dc1c..66525c37 100644 --- a/lib/utils/promote.ts +++ b/lib/utils/promote.ts @@ -39,8 +39,14 @@ export async function join( app.device_type = deviceType; } + logger.logDebug('Determining device OS version...'); + const deviceOsVersion = await getOsVersion(deviceIp); + logger.logDebug(`Device OS version: ${deviceOsVersion}`); + logger.logDebug('Generating application config...'); - const config = await generateApplicationConfig(sdk, app); + const config = await generateApplicationConfig(sdk, app, { + version: deviceOsVersion, + }); logger.logDebug(`Using config: ${JSON.stringify(config, null, 2)}`); logger.logDebug('Configuring...'); @@ -123,6 +129,15 @@ async function getDeviceType(deviceIp: string): Promise { return match[1]; } +async function getOsVersion(deviceIp: string): Promise { + const output = await execBuffered(deviceIp, 'cat /etc/os-release'); + const match = /^VERSION_ID="([^"]+)"$/m.exec(output); + if (!match) { + throw new Error('Failed to determine OS version ID'); + } + return match[1]; +} + async function getOrSelectLocalDevice(deviceIp?: string): Promise { if (deviceIp) { return deviceIp; @@ -304,14 +319,21 @@ async function createApplication( }); } -async function generateApplicationConfig(sdk: ResinSDK, app: Application) { +async function generateApplicationConfig( + sdk: ResinSDK, + app: Application, + options: { version: string }, +) { const form = await import('resin-cli-form'); const { generateApplicationConfig: configGen } = await import('./config'); const manifest = await sdk.models.device.getManifestBySlug(app.device_type); const opts = manifest.options && manifest.options.filter(opt => opt.name !== 'network'); - const values = await form.run(opts); + const values = { + ...(await form.run(opts)), + ...options, + }; const config = await configGen(app, values); if (config.connectivity === 'connman') {