mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-18 21:27:51 +00:00
Convert lib/actions/local/configure.coffee to javascript
Change-type: patch
This commit is contained in:
parent
cd59496f11
commit
9447195c26
@ -1,236 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
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.
|
||||
###
|
||||
|
||||
BOOT_PARTITION = 1
|
||||
CONNECTIONS_FOLDER = '/system-connections'
|
||||
|
||||
getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
mapper: [
|
||||
{
|
||||
template:
|
||||
persistentLogging: '{{persistentLogging}}'
|
||||
domain: [
|
||||
[ 'config_json', 'persistentLogging' ]
|
||||
]
|
||||
}
|
||||
{
|
||||
template:
|
||||
hostname: '{{hostname}}'
|
||||
domain: [
|
||||
[ 'config_json', 'hostname' ]
|
||||
]
|
||||
}
|
||||
{
|
||||
template:
|
||||
wifi:
|
||||
ssid: '{{networkSsid}}'
|
||||
'wifi-security':
|
||||
psk: '{{networkKey}}'
|
||||
domain: [
|
||||
[ 'system_connections', connnectionFileName, 'wifi' ]
|
||||
[ 'system_connections', connnectionFileName, 'wifi-security' ]
|
||||
]
|
||||
}
|
||||
]
|
||||
files:
|
||||
system_connections:
|
||||
fileset: true
|
||||
type: 'ini'
|
||||
location:
|
||||
path: CONNECTIONS_FOLDER.slice(1)
|
||||
# Reconfix still uses the older resin-image-fs, so still needs an
|
||||
# object-based partition definition.
|
||||
partition: BOOT_PARTITION
|
||||
config_json:
|
||||
type: 'json'
|
||||
location:
|
||||
path: 'config.json'
|
||||
partition: BOOT_PARTITION
|
||||
|
||||
inquirerOptions = (data) -> [
|
||||
{
|
||||
message: 'Network SSID'
|
||||
type: 'input'
|
||||
name: 'networkSsid'
|
||||
default: data.networkSsid
|
||||
}
|
||||
{
|
||||
message: 'Network Key'
|
||||
type: 'input'
|
||||
name: 'networkKey'
|
||||
default: data.networkKey
|
||||
}
|
||||
{
|
||||
message: 'Do you want to set advanced settings?'
|
||||
type: 'confirm'
|
||||
name: 'advancedSettings'
|
||||
default: false
|
||||
}
|
||||
{
|
||||
message: 'Device Hostname'
|
||||
type: 'input'
|
||||
name: 'hostname'
|
||||
default: data.hostname,
|
||||
when: (answers) ->
|
||||
answers.advancedSettings
|
||||
}
|
||||
{
|
||||
message: 'Do you want to enable persistent logging?'
|
||||
type: 'confirm'
|
||||
name: 'persistentLogging'
|
||||
default: data.persistentLogging
|
||||
when: (answers) ->
|
||||
answers.advancedSettings
|
||||
}
|
||||
]
|
||||
|
||||
getConfiguration = (data) ->
|
||||
_ = require('lodash')
|
||||
inquirer = require('inquirer')
|
||||
|
||||
# `persistentLogging` can be `undefined`, so we want
|
||||
# to make sure that case defaults to `false`
|
||||
data = _.assign data,
|
||||
persistentLogging: data.persistentLogging or false
|
||||
|
||||
inquirer.prompt(inquirerOptions(data))
|
||||
.then (answers) ->
|
||||
return _.merge(data, answers)
|
||||
|
||||
# Taken from https://goo.gl/kr1kCt
|
||||
CONNECTION_FILE = '''
|
||||
[connection]
|
||||
id=resin-wifi
|
||||
type=wifi
|
||||
|
||||
[wifi]
|
||||
hidden=true
|
||||
mode=infrastructure
|
||||
ssid=My_Wifi_Ssid
|
||||
|
||||
[wifi-security]
|
||||
auth-alg=open
|
||||
key-mgmt=wpa-psk
|
||||
psk=super_secret_wifi_password
|
||||
|
||||
[ipv4]
|
||||
method=auto
|
||||
|
||||
[ipv6]
|
||||
addr-gen-mode=stable-privacy
|
||||
method=auto
|
||||
'''
|
||||
|
||||
###
|
||||
* if the `resin-wifi` file exists (previously configured image or downloaded from the UI) it's used and reconfigured
|
||||
* if the `resin-sample.ignore` exists it's copied to `resin-wifi`
|
||||
* if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually)
|
||||
* otherwise, the new file is created
|
||||
###
|
||||
prepareConnectionFile = (target) ->
|
||||
_ = require('lodash')
|
||||
imagefs = require('resin-image-fs')
|
||||
|
||||
imagefs.listDirectory
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: CONNECTIONS_FOLDER
|
||||
.then (files) ->
|
||||
# The required file already exists
|
||||
if _.includes(files, 'resin-wifi')
|
||||
return null
|
||||
|
||||
# Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
if _.includes(files, 'resin-sample.ignore')
|
||||
return imagefs.copy
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-sample.ignore"
|
||||
,
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-wifi"
|
||||
.thenReturn(null)
|
||||
|
||||
# Legacy mode, to be removed later
|
||||
# We return the file name override from this branch
|
||||
# When it is removed the following cleanup should be done:
|
||||
# * delete all the null returns from this method
|
||||
# * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
|
||||
# * drop the final `then` from this method
|
||||
# * adapt the code in the main listener to not receive the config from this method, and use that constant instead
|
||||
if _.includes(files, 'resin-sample')
|
||||
return 'resin-sample'
|
||||
|
||||
# In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||
return imagefs.writeFile
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-wifi"
|
||||
, CONNECTION_FILE
|
||||
.thenReturn(null)
|
||||
|
||||
.then (connectionFileName) ->
|
||||
return getConfigurationSchema(connectionFileName)
|
||||
|
||||
removeHostname = (schema) ->
|
||||
_ = require('lodash')
|
||||
schema.mapper = _.reject schema.mapper, (mapper) ->
|
||||
_.isEqual(Object.keys(mapper.template), ['hostname'])
|
||||
|
||||
module.exports =
|
||||
signature: 'local configure <target>'
|
||||
description: '(Re)configure a balenaOS drive or image'
|
||||
help: '''
|
||||
Use this command to configure or reconfigure a balenaOS drive or image.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local configure /dev/sdc
|
||||
$ balena local configure path/to/image.img
|
||||
'''
|
||||
root: true
|
||||
action: (params, options) ->
|
||||
Promise = require('bluebird')
|
||||
path = require('path')
|
||||
umount = require('umount')
|
||||
umountAsync = Promise.promisify(umount.umount)
|
||||
isMountedAsync = Promise.promisify(umount.isMounted)
|
||||
reconfix = require('reconfix')
|
||||
denymount = Promise.promisify(require('denymount'))
|
||||
|
||||
prepareConnectionFile(params.target)
|
||||
.tap ->
|
||||
isMountedAsync(params.target).then (isMounted) ->
|
||||
return if not isMounted
|
||||
umountAsync(params.target)
|
||||
.then (configurationSchema) ->
|
||||
dmOpts = {}
|
||||
if process.pkg
|
||||
# when running in a standalone pkg install, the 'denymount'
|
||||
# executable is placed on the same folder as process.execPath
|
||||
dmOpts.executablePath = path.join(path.dirname(process.execPath), 'denymount')
|
||||
dmHandler = (cb) ->
|
||||
reconfix.readConfiguration(configurationSchema, params.target)
|
||||
.then(getConfiguration)
|
||||
.then (answers) ->
|
||||
if not answers.hostname
|
||||
removeHostname(configurationSchema)
|
||||
reconfix.writeConfiguration(configurationSchema, answers, params.target)
|
||||
.asCallback(cb)
|
||||
denymount params.target, dmHandler, dmOpts
|
||||
.then ->
|
||||
console.log('Done!')
|
285
lib/actions/local/configure.js
Normal file
285
lib/actions/local/configure.js
Normal file
@ -0,0 +1,285 @@
|
||||
/*
|
||||
Copyright 2017-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.
|
||||
*/
|
||||
|
||||
const BOOT_PARTITION = 1;
|
||||
const CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
const getConfigurationSchema = function(connnectionFileName) {
|
||||
if (connnectionFileName == null) {
|
||||
connnectionFileName = 'resin-wifi';
|
||||
}
|
||||
return {
|
||||
mapper: [
|
||||
{
|
||||
template: {
|
||||
persistentLogging: '{{persistentLogging}}',
|
||||
},
|
||||
domain: [['config_json', 'persistentLogging']],
|
||||
},
|
||||
{
|
||||
template: {
|
||||
hostname: '{{hostname}}',
|
||||
},
|
||||
domain: [['config_json', 'hostname']],
|
||||
},
|
||||
{
|
||||
template: {
|
||||
wifi: {
|
||||
ssid: '{{networkSsid}}',
|
||||
},
|
||||
'wifi-security': {
|
||||
psk: '{{networkKey}}',
|
||||
},
|
||||
},
|
||||
domain: [
|
||||
['system_connections', connnectionFileName, 'wifi'],
|
||||
['system_connections', connnectionFileName, 'wifi-security'],
|
||||
],
|
||||
},
|
||||
],
|
||||
files: {
|
||||
system_connections: {
|
||||
fileset: true,
|
||||
type: 'ini',
|
||||
location: {
|
||||
path: CONNECTIONS_FOLDER.slice(1),
|
||||
// Reconfix still uses the older resin-image-fs, so still needs an
|
||||
// object-based partition definition.
|
||||
partition: BOOT_PARTITION,
|
||||
},
|
||||
},
|
||||
config_json: {
|
||||
type: 'json',
|
||||
location: {
|
||||
path: 'config.json',
|
||||
partition: BOOT_PARTITION,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const inquirerOptions = data => [
|
||||
{
|
||||
message: 'Network SSID',
|
||||
type: 'input',
|
||||
name: 'networkSsid',
|
||||
default: data.networkSsid,
|
||||
},
|
||||
{
|
||||
message: 'Network Key',
|
||||
type: 'input',
|
||||
name: 'networkKey',
|
||||
default: data.networkKey,
|
||||
},
|
||||
{
|
||||
message: 'Do you want to set advanced settings?',
|
||||
type: 'confirm',
|
||||
name: 'advancedSettings',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
message: 'Device Hostname',
|
||||
type: 'input',
|
||||
name: 'hostname',
|
||||
default: data.hostname,
|
||||
when(answers) {
|
||||
return answers.advancedSettings;
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Do you want to enable persistent logging?',
|
||||
type: 'confirm',
|
||||
name: 'persistentLogging',
|
||||
default: data.persistentLogging,
|
||||
when(answers) {
|
||||
return answers.advancedSettings;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getConfiguration = function(data) {
|
||||
const _ = require('lodash');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
// `persistentLogging` can be `undefined`, so we want
|
||||
// to make sure that case defaults to `false`
|
||||
data = _.assign(data, { persistentLogging: data.persistentLogging || false });
|
||||
|
||||
return inquirer
|
||||
.prompt(inquirerOptions(data))
|
||||
.then(answers => _.merge(data, answers));
|
||||
};
|
||||
|
||||
// Taken from https://goo.gl/kr1kCt
|
||||
const CONNECTION_FILE = `\
|
||||
[connection]
|
||||
id=resin-wifi
|
||||
type=wifi
|
||||
|
||||
[wifi]
|
||||
hidden=true
|
||||
mode=infrastructure
|
||||
ssid=My_Wifi_Ssid
|
||||
|
||||
[wifi-security]
|
||||
auth-alg=open
|
||||
key-mgmt=wpa-psk
|
||||
psk=super_secret_wifi_password
|
||||
|
||||
[ipv4]
|
||||
method=auto
|
||||
|
||||
[ipv6]
|
||||
addr-gen-mode=stable-privacy
|
||||
method=auto\
|
||||
`;
|
||||
|
||||
/*
|
||||
* if the `resin-wifi` file exists (previously configured image or downloaded from the UI) it's used and reconfigured
|
||||
* if the `resin-sample.ignore` exists it's copied to `resin-wifi`
|
||||
* if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually)
|
||||
* otherwise, the new file is created
|
||||
*/
|
||||
const prepareConnectionFile = function(target) {
|
||||
const _ = require('lodash');
|
||||
const imagefs = require('resin-image-fs');
|
||||
|
||||
return imagefs
|
||||
.listDirectory({
|
||||
image: target,
|
||||
partition: BOOT_PARTITION,
|
||||
path: CONNECTIONS_FOLDER,
|
||||
})
|
||||
.then(function(files) {
|
||||
// The required file already exists
|
||||
if (_.includes(files, 'resin-wifi')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
if (_.includes(files, 'resin-sample.ignore')) {
|
||||
return imagefs
|
||||
.copy(
|
||||
{
|
||||
image: target,
|
||||
partition: BOOT_PARTITION,
|
||||
path: `${CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
||||
},
|
||||
{
|
||||
image: target,
|
||||
partition: BOOT_PARTITION,
|
||||
path: `${CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
},
|
||||
)
|
||||
.thenReturn(null);
|
||||
}
|
||||
|
||||
// Legacy mode, to be removed later
|
||||
// We return the file name override from this branch
|
||||
// When it is removed the following cleanup should be done:
|
||||
// * delete all the null returns from this method
|
||||
// * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
|
||||
// * drop the final `then` from this method
|
||||
// * adapt the code in the main listener to not receive the config from this method, and use that constant instead
|
||||
if (_.includes(files, 'resin-sample')) {
|
||||
return 'resin-sample';
|
||||
}
|
||||
|
||||
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||
return imagefs
|
||||
.writeFile(
|
||||
{
|
||||
image: target,
|
||||
partition: BOOT_PARTITION,
|
||||
path: `${CONNECTIONS_FOLDER}/resin-wifi`,
|
||||
},
|
||||
CONNECTION_FILE,
|
||||
)
|
||||
.thenReturn(null);
|
||||
})
|
||||
.then(connectionFileName => getConfigurationSchema(connectionFileName));
|
||||
};
|
||||
|
||||
const removeHostname = function(schema) {
|
||||
const _ = require('lodash');
|
||||
schema.mapper = _.reject(schema.mapper, mapper =>
|
||||
_.isEqual(Object.keys(mapper.template), ['hostname']),
|
||||
);
|
||||
};
|
||||
|
||||
export const configure = {
|
||||
signature: 'local configure <target>',
|
||||
description: '(Re)configure a balenaOS drive or image',
|
||||
help: `\
|
||||
Use this command to configure or reconfigure a balenaOS drive or image.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local configure /dev/sdc
|
||||
$ balena local configure path/to/image.img\
|
||||
`,
|
||||
root: true,
|
||||
action(params) {
|
||||
const Promise = require('bluebird');
|
||||
const path = require('path');
|
||||
const umount = require('umount');
|
||||
const umountAsync = Promise.promisify(umount.umount);
|
||||
const isMountedAsync = Promise.promisify(umount.isMounted);
|
||||
const reconfix = require('reconfix');
|
||||
const denymount = Promise.promisify(require('denymount'));
|
||||
|
||||
return prepareConnectionFile(params.target)
|
||||
.tap(() =>
|
||||
isMountedAsync(params.target).then(function(isMounted) {
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
return umountAsync(params.target);
|
||||
}),
|
||||
)
|
||||
.then(function(configurationSchema) {
|
||||
const dmOpts = {};
|
||||
if (process.pkg) {
|
||||
// when running in a standalone pkg install, the 'denymount'
|
||||
// executable is placed on the same folder as process.execPath
|
||||
dmOpts.executablePath = path.join(
|
||||
path.dirname(process.execPath),
|
||||
'denymount',
|
||||
);
|
||||
}
|
||||
const dmHandler = cb =>
|
||||
reconfix
|
||||
.readConfiguration(configurationSchema, params.target)
|
||||
.then(getConfiguration)
|
||||
.then(function(answers) {
|
||||
if (!answers.hostname) {
|
||||
removeHostname(configurationSchema);
|
||||
}
|
||||
return reconfix.writeConfiguration(
|
||||
configurationSchema,
|
||||
answers,
|
||||
params.target,
|
||||
);
|
||||
})
|
||||
.asCallback(cb);
|
||||
return denymount(params.target, dmHandler, dmOpts);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Done!');
|
||||
});
|
||||
},
|
||||
};
|
@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
exports.configure = require('./configure')
|
||||
exports.configure = require('./configure').configure
|
||||
exports.flash = require('./flash').flash
|
||||
|
26
typings/denymount/index.d.ts
vendored
Normal file
26
typings/denymount/index.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
declare module 'denymount' {
|
||||
const denymount: (
|
||||
target: string,
|
||||
handler: (cb: typeof callback) => void,
|
||||
opts?: { autoMountOnSuccess?: boolean; executablePath?: string },
|
||||
callback: (err?: any) => void,
|
||||
) => void;
|
||||
export = denymount;
|
||||
}
|
7
typings/pkg/index.d.ts
vendored
7
typings/pkg/index.d.ts
vendored
@ -15,6 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// tslint:disable-next-line:no-namespace
|
||||
declare namespace NodeJS {
|
||||
interface Process {
|
||||
pkg?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'pkg' {
|
||||
export function exec(args: string[]): Promise<void>;
|
||||
}
|
||||
|
15
typings/resin-image-fs/index.d.ts
vendored
15
typings/resin-image-fs/index.d.ts
vendored
@ -18,5 +18,20 @@
|
||||
declare module 'resin-image-fs' {
|
||||
import Promise = require('bluebird');
|
||||
|
||||
export interface ImageDefinition {
|
||||
image: string;
|
||||
partition: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export function readFile(options: {}): Promise<string>;
|
||||
export function writeFile(
|
||||
definition: ImageDefinition,
|
||||
contents: string,
|
||||
): Promise<void>;
|
||||
export function copy(
|
||||
input: ImageDefinition,
|
||||
output: ImageDefinition,
|
||||
): Promise<void>;
|
||||
export function listDirectory(definition: ImageDefinition): Promise<string[]>;
|
||||
}
|
||||
|
27
typings/umount/index.d.ts
vendored
Normal file
27
typings/umount/index.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
declare module 'umount' {
|
||||
export const umount: (
|
||||
device: string,
|
||||
callback: (err?: any, stdout?: any, stderr?: any) => void,
|
||||
) => void;
|
||||
export const isMounted: (
|
||||
device: string,
|
||||
callback: (err?: any, isMounted?: boolean) => void,
|
||||
) => void;
|
||||
}
|
Loading…
Reference in New Issue
Block a user