mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-18 21:27:51 +00:00
Convert local configure
to oclif, typescript
Change-type: patch Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
parent
457eff1d43
commit
4f831ef443
@ -138,8 +138,8 @@ const capitanoDoc = {
|
|||||||
{
|
{
|
||||||
title: 'Local',
|
title: 'Local',
|
||||||
files: [
|
files: [
|
||||||
|
'build/actions-oclif/local/configure.js',
|
||||||
'build/actions-oclif/local/flash.js',
|
'build/actions-oclif/local/flash.js',
|
||||||
'build/actions/local/index.js',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -250,8 +250,8 @@ Users are encouraged to regularly update the balena CLI to the latest version.
|
|||||||
|
|
||||||
- Local
|
- Local
|
||||||
|
|
||||||
- [local flash <image>](#local-flash-image)
|
|
||||||
- [local configure <target>](#local-configure-target)
|
- [local configure <target>](#local-configure-target)
|
||||||
|
- [local flash <image>](#local-flash-image)
|
||||||
|
|
||||||
- Deploy
|
- Deploy
|
||||||
|
|
||||||
@ -2161,6 +2161,23 @@ Examples:
|
|||||||
|
|
||||||
# Local
|
# Local
|
||||||
|
|
||||||
|
## local configure <target>
|
||||||
|
|
||||||
|
Configure or reconfigure a balenaOS drive or image.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ balena local configure /dev/sdc
|
||||||
|
$ balena local configure path/to/image.img
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
#### TARGET
|
||||||
|
|
||||||
|
path of drive or image to configure
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
## local flash <image>
|
## local flash <image>
|
||||||
|
|
||||||
Flash a balenaOS image to a drive.
|
Flash a balenaOS image to a drive.
|
||||||
@ -2191,15 +2208,6 @@ drive to flash
|
|||||||
|
|
||||||
answer "yes" to all questions (non interactive use)
|
answer "yes" to all questions (non interactive use)
|
||||||
|
|
||||||
## local configure <target>
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Deploy
|
# Deploy
|
||||||
|
|
||||||
## build [source]
|
## build [source]
|
||||||
|
328
lib/actions-oclif/local/configure.ts
Normal file
328
lib/actions-oclif/local/configure.ts
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
/**
|
||||||
|
* @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 Command from '../../command';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
|
import { stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
interface FlagsDef {
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LocalConfigureCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
(Re)configure a balenaOS drive or image.
|
||||||
|
|
||||||
|
Configure or reconfigure a balenaOS drive or image.
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static examples = [
|
||||||
|
'$ balena local configure /dev/sdc',
|
||||||
|
'$ balena local configure path/to/image.img',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = [
|
||||||
|
{
|
||||||
|
name: 'target',
|
||||||
|
description: 'path of drive or image to configure',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'local configure <target>';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
help: cf.help,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static root = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
|
||||||
|
|
||||||
|
const Bluebird = await import('bluebird');
|
||||||
|
const path = await import('path');
|
||||||
|
const umount = await import('umount');
|
||||||
|
const umountAsync = Bluebird.promisify(umount.umount);
|
||||||
|
const isMountedAsync = Bluebird.promisify(umount.isMounted);
|
||||||
|
const reconfix = await import('reconfix');
|
||||||
|
const denymount = Bluebird.promisify(await import('denymount'));
|
||||||
|
const Logger = await import('../../utils/logger');
|
||||||
|
|
||||||
|
const logger = Logger.getLogger();
|
||||||
|
|
||||||
|
const configurationSchema = await this.prepareConnectionFile(params.target);
|
||||||
|
|
||||||
|
if (await isMountedAsync(params.target)) {
|
||||||
|
await umountAsync(params.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dmOpts: any = {};
|
||||||
|
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: () => void) =>
|
||||||
|
reconfix
|
||||||
|
.readConfiguration(configurationSchema, params.target)
|
||||||
|
.tap((config: any) => {
|
||||||
|
logger.logDebug('Current config:');
|
||||||
|
logger.logDebug(JSON.stringify(config));
|
||||||
|
})
|
||||||
|
.then((config: any) => this.getConfiguration(config))
|
||||||
|
.tap((config: any) => {
|
||||||
|
logger.logDebug('New config:');
|
||||||
|
logger.logDebug(JSON.stringify(config));
|
||||||
|
})
|
||||||
|
.then(async (answers: any) => {
|
||||||
|
if (!answers.hostname) {
|
||||||
|
await this.removeHostname(configurationSchema);
|
||||||
|
}
|
||||||
|
return reconfix.writeConfiguration(
|
||||||
|
configurationSchema,
|
||||||
|
answers,
|
||||||
|
params.target,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.asCallback(cb);
|
||||||
|
|
||||||
|
await denymount(params.target, dmHandler, dmOpts);
|
||||||
|
|
||||||
|
console.log('Done!');
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly BOOT_PARTITION = 1;
|
||||||
|
readonly CONNECTIONS_FOLDER = '/system-connections';
|
||||||
|
|
||||||
|
getConfigurationSchema(connectionFileName?: string) {
|
||||||
|
if (connectionFileName == null) {
|
||||||
|
connectionFileName = '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', connectionFileName, 'wifi'],
|
||||||
|
['system_connections', connectionFileName, 'wifi-security'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
files: {
|
||||||
|
system_connections: {
|
||||||
|
fileset: true,
|
||||||
|
type: 'ini',
|
||||||
|
location: {
|
||||||
|
path: this.CONNECTIONS_FOLDER.slice(1),
|
||||||
|
// Reconfix still uses the older resin-image-fs, so still needs an
|
||||||
|
// object-based partition definition.
|
||||||
|
partition: this.BOOT_PARTITION,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config_json: {
|
||||||
|
type: 'json',
|
||||||
|
location: {
|
||||||
|
path: 'config.json',
|
||||||
|
partition: this.BOOT_PARTITION,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inquirerOptions = (data: any) => [
|
||||||
|
{
|
||||||
|
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: any) {
|
||||||
|
return answers.advancedSettings;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Do you want to enable persistent logging?',
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'persistentLogging',
|
||||||
|
default: data.persistentLogging,
|
||||||
|
when(answers: any) {
|
||||||
|
return answers.advancedSettings;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
getConfiguration = async (data: any) => {
|
||||||
|
const _ = await import('lodash');
|
||||||
|
const inquirer = await import('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(this.inquirerOptions(data))
|
||||||
|
.then((answers: any) => _.merge(data, answers));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Taken from https://goo.gl/kr1kCt
|
||||||
|
readonly CONNECTION_FILE = stripIndent`
|
||||||
|
[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
|
||||||
|
*/
|
||||||
|
async prepareConnectionFile(target: string) {
|
||||||
|
const _ = await import('lodash');
|
||||||
|
const imagefs = await import('resin-image-fs');
|
||||||
|
|
||||||
|
return imagefs
|
||||||
|
.listDirectory({
|
||||||
|
image: target,
|
||||||
|
partition: this.BOOT_PARTITION,
|
||||||
|
path: this.CONNECTIONS_FOLDER,
|
||||||
|
})
|
||||||
|
.then((files: string[]) => {
|
||||||
|
// 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: this.BOOT_PARTITION,
|
||||||
|
path: `${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: target,
|
||||||
|
partition: this.BOOT_PARTITION,
|
||||||
|
path: `${this.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: this.BOOT_PARTITION,
|
||||||
|
path: `${this.CONNECTIONS_FOLDER}/resin-wifi`,
|
||||||
|
},
|
||||||
|
this.CONNECTION_FILE,
|
||||||
|
)
|
||||||
|
.thenReturn(null);
|
||||||
|
})
|
||||||
|
.then((connectionFileName) =>
|
||||||
|
this.getConfigurationSchema(connectionFileName || undefined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeHostname(schema: any) {
|
||||||
|
const _ = await import('lodash');
|
||||||
|
schema.mapper = _.reject(schema.mapper, (mapper: any) =>
|
||||||
|
_.isEqual(Object.keys(mapper.template), ['hostname']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -65,11 +65,11 @@ export default class ScanCmd extends Command {
|
|||||||
const { discover } = await import('balena-sync');
|
const { discover } = await import('balena-sync');
|
||||||
const prettyjson = await import('prettyjson');
|
const prettyjson = await import('prettyjson');
|
||||||
const { ExpectedError } = await import('../errors');
|
const { ExpectedError } = await import('../errors');
|
||||||
const { dockerPort, dockerTimeout } = await import(
|
|
||||||
'../actions/local/common'
|
|
||||||
);
|
|
||||||
const dockerUtils = await import('../utils/docker');
|
const dockerUtils = await import('../utils/docker');
|
||||||
|
|
||||||
|
const dockerPort = 2375;
|
||||||
|
const dockerTimeout = 2000;
|
||||||
|
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
|
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
|
||||||
|
|
||||||
const discoverTimeout =
|
const discoverTimeout =
|
||||||
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
|
|
||||||
export * as config from './config';
|
export * as config from './config';
|
||||||
export * as help from './help';
|
export * as help from './help';
|
||||||
export * as local from './local';
|
|
||||||
export * as os from './os';
|
export * as os from './os';
|
||||||
export * as push from './push';
|
export * as push from './push';
|
||||||
|
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
import * as Bluebird from 'bluebird';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as dockerUtils from '../../utils/docker';
|
|
||||||
import { exitWithExpectedError } from '../../errors';
|
|
||||||
import { getChalk, getCliForm } from '../../utils/lazy';
|
|
||||||
|
|
||||||
export const dockerPort = 2375;
|
|
||||||
export const dockerTimeout = 2000;
|
|
||||||
|
|
||||||
export const filterOutSupervisorContainer = function (container) {
|
|
||||||
for (const name of container.Names) {
|
|
||||||
if (
|
|
||||||
name.includes('resin_supervisor') ||
|
|
||||||
name.includes('balena_supervisor')
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const selectContainerFromDevice = Bluebird.method(function (
|
|
||||||
deviceIp,
|
|
||||||
filterSupervisor,
|
|
||||||
) {
|
|
||||||
if (filterSupervisor == null) {
|
|
||||||
filterSupervisor = false;
|
|
||||||
}
|
|
||||||
const docker = dockerUtils.createClient({
|
|
||||||
host: deviceIp,
|
|
||||||
port: dockerPort,
|
|
||||||
timeout: dockerTimeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
// List all containers, including those not running
|
|
||||||
return docker.listContainers({ all: true }).then(function (containers) {
|
|
||||||
containers = containers.filter(function (container) {
|
|
||||||
if (!filterSupervisor) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return filterOutSupervisorContainer(container);
|
|
||||||
});
|
|
||||||
if (_.isEmpty(containers)) {
|
|
||||||
exitWithExpectedError(`No containers found in ${deviceIp}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getCliForm().ask({
|
|
||||||
message: 'Select a container',
|
|
||||||
type: 'list',
|
|
||||||
choices: _.map(containers, function (container) {
|
|
||||||
const containerName = container.Names?.[0] || 'Untitled';
|
|
||||||
const shortContainerId = ('' + container.Id).substr(0, 11);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: `${containerName} (${shortContainerId})`,
|
|
||||||
value: container.Id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const pipeContainerStream = Bluebird.method(function ({
|
|
||||||
deviceIp,
|
|
||||||
name,
|
|
||||||
outStream,
|
|
||||||
follow,
|
|
||||||
}) {
|
|
||||||
if (follow == null) {
|
|
||||||
follow = false;
|
|
||||||
}
|
|
||||||
const docker = dockerUtils.createClient({ host: deviceIp, port: dockerPort });
|
|
||||||
|
|
||||||
const container = docker.getContainer(name);
|
|
||||||
return container
|
|
||||||
.inspect()
|
|
||||||
.then((containerInfo) => containerInfo?.State?.Running)
|
|
||||||
.then((isRunning) =>
|
|
||||||
container.attach({
|
|
||||||
logs: !follow || !isRunning,
|
|
||||||
stream: follow && isRunning,
|
|
||||||
stdout: true,
|
|
||||||
stderr: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then((containerStream) => containerStream.pipe(outStream))
|
|
||||||
.catch(function (err) {
|
|
||||||
err = '' + err.statusCode;
|
|
||||||
if (err === '404') {
|
|
||||||
return console.log(
|
|
||||||
getChalk().red.bold(`Container '${name}' not found.`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,285 +0,0 @@
|
|||||||
/*
|
|
||||||
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 Bluebird = require('bluebird');
|
|
||||||
const path = require('path');
|
|
||||||
const umount = require('umount');
|
|
||||||
const umountAsync = Bluebird.promisify(umount.umount);
|
|
||||||
const isMountedAsync = Bluebird.promisify(umount.isMounted);
|
|
||||||
const reconfix = require('reconfix');
|
|
||||||
const denymount = Bluebird.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!');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { configure } from './configure';
|
|
@ -62,9 +62,6 @@ capitano.command(actions.config.generate);
|
|||||||
// ---------- Preload Module ----------
|
// ---------- Preload Module ----------
|
||||||
capitano.command(actions.preload);
|
capitano.command(actions.preload);
|
||||||
|
|
||||||
// ---------- Local balenaOS Module ----------
|
|
||||||
capitano.command(actions.local.configure);
|
|
||||||
|
|
||||||
// ------------ Local build and deploy -------
|
// ------------ Local build and deploy -------
|
||||||
capitano.command(actions.build);
|
capitano.command(actions.build);
|
||||||
capitano.command(actions.deploy);
|
capitano.command(actions.deploy);
|
||||||
|
@ -173,6 +173,7 @@ export const convertedCommands = [
|
|||||||
'key:add',
|
'key:add',
|
||||||
'key:rm',
|
'key:rm',
|
||||||
'leave',
|
'leave',
|
||||||
|
'local:configure',
|
||||||
'local:flash',
|
'local:flash',
|
||||||
'login',
|
'login',
|
||||||
'logout',
|
'logout',
|
||||||
|
18
typings/reconfix/index.d.ts
vendored
Normal file
18
typings/reconfix/index.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @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 'reconfix';
|
Loading…
Reference in New Issue
Block a user