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 <thodoris@resin.io>
This commit is contained in:
Thodoris Greasidis 2018-10-15 19:03:21 +02:00 committed by Tim Perry
parent 13729ec4b6
commit 4d42f74c0c
7 changed files with 87 additions and 89 deletions

View File

@ -142,7 +142,7 @@ environment variable (in the same standard URL format).
- [os versions &#60;type&#62;](#os-versions-type-)
- [os download &#60;type&#62;](#os-download-type-)
- [os build-config &#60;image&#62; &#60;device-type&#62;](#os-build-config-image-device-type-)
- [os configure &#60;image&#62; [uuid] [deviceApiKey]](#os-configure-image-uuid-deviceapikey-)
- [os configure &#60;image&#62;](#os-configure-image-)
- [os initialize &#60;image&#62;](#os-initialize-image-)
- Config
@ -965,13 +965,12 @@ show advanced configuration options
the path to the output JSON file
## os configure &#60;image&#62; [uuid] [deviceApiKey]
## os configure &#60;image&#62;
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

View File

@ -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,

View File

@ -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

View File

@ -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'

View File

@ -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 <image> [uuid] [deviceApiKey]'
signature: 'os configure <image>'
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 <uuid>
* An application, with --app <appname>
* [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 <uuid> 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')

View File

@ -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<string | void> {
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<ImgConfig> {
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)

View File

@ -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<string> {
return match[1];
}
async function getOsVersion(deviceIp: string): Promise<string> {
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<string> {
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') {