diff --git a/bolt/README.md b/bolt/README.md index f349e5a..75c4dcb 100644 --- a/bolt/README.md +++ b/bolt/README.md @@ -47,6 +47,11 @@ Usage: bolt run Execute a bolt package on a remote device + --develop Run with elevated privileges to simplify debugging + --clear-storage Clear persistent storage before running the package + --uid= Run with the specified user ID + --gid= Run with the specified group ID + --userns= Enable/disable user namespace Where: target Basename of a file named .bolt.json, which defines build instructions diff --git a/bolt/src/bolt.cjs b/bolt/src/bolt.cjs index 297a002..08acb33 100644 --- a/bolt/src/bolt.cjs +++ b/bolt/src/bolt.cjs @@ -22,7 +22,7 @@ const { diff } = require('./diff.cjs'); const { extract } = require('./extract.cjs'); const { pack } = require('./pack.cjs'); const { push } = require('./push.cjs'); -const { run } = require('./run.cjs'); +const { run, runOptions } = require('./run.cjs'); const { make, makeOptions } = require('./make.cjs'); function help() { @@ -47,6 +47,11 @@ Usage: bolt run Execute a bolt package on a remote device + --develop Run with elevated privileges to simplify debugging + --clear-storage Clear persistent storage before running the package + --uid= Run with the specified user ID + --gid= Run with the specified group ID + --userns= Enable/disable user namespace Where: target Basename of a file named .bolt.json, which defines build instructions @@ -72,7 +77,7 @@ const commands = { extract: { args: 2, handler: extract }, pack: { args: 2, handler: pack }, push: { args: 2, handler: push }, - run: { args: 2, handler: run }, + run: { args: 2, handler: run, options: runOptions }, make: { args: 1, handler: make, options: makeOptions }, }; diff --git a/bolt/src/config.cjs b/bolt/src/config.cjs index aebf536..9a8e6ea 100644 --- a/bolt/src/config.cjs +++ b/bolt/src/config.cjs @@ -24,3 +24,6 @@ exports.REMOTE_BUNDLES_DIR = "/data/bolt/bundles"; exports.REMOTE_GPU_LAYER_FS = "/usr/share/gpu-layer/rootfs"; exports.REMOTE_GPU_CONFIG = "/usr/share/gpu-layer/config.json"; exports.AI2_MANAGERS_ENABLED_FILE = "/opt/ai2managers"; +// select random UID and GID (34567) to avoid conflicts with existing users/groups +exports.DEFAULT_UID = 34567; +exports.DEFAULT_GID = 34567; diff --git a/bolt/src/run.cjs b/bolt/src/run.cjs index 7ac1b68..022aef8 100644 --- a/bolt/src/run.cjs +++ b/bolt/src/run.cjs @@ -166,7 +166,7 @@ function setupResources(remote, pkg) { } } -function prepareBundle(remote, pkg, bundleConfig, layers) { +function prepareBundle(remote, pkg, bundleConfig, layers, options) { const bundleDir = remote.getPkgBundleDir(pkg); const bundleRootfsDir = bundleDir + "/rootfs"; @@ -174,10 +174,12 @@ function prepareBundle(remote, pkg, bundleConfig, layers) { remote.unmount(bundleRootfsDir); } - /* remove mount points inside /usr/lib in rw overlay to cleanup leftovers from previous runs */ - remote.rmdir(`${bundleDir}/rw/upper/usr/lib`); + if (options.clearStorage) { + remote.rmdir(`${bundleDir}`); + } remote.mkdir(`${bundleRootfsDir} ${bundleDir}/rw/{upper,work}`); + remote.exec(`chmod 777 ${bundleDir}/rw/{upper,work}`); remote.exec(`mount -t overlay overlay -o lowerdir=${layers.join(":")},upperdir=${bundleDir}/rw/upper,workdir=${bundleDir}/rw/work ${bundleRootfsDir}`); remote.storeObject(`${bundleDir}/config.json`, bundleConfig); @@ -251,7 +253,7 @@ function addDeviceGPULayer(remote, bundleConfig, layerDirs) { return result; } -function run(remoteName, pkg) { +function run(remoteName, pkg, options) { const remote = new Remote(remoteName); const configs = getConfigs(remote, pkg); @@ -260,7 +262,7 @@ function run(remoteName, pkg) { console.log(`Running ${pkg} using:`); console.log(`${JSON.stringify(configs, null, 2)}`); - const bundleConfig = makeTemplate(); + const bundleConfig = makeTemplate(options); for (const { pkg, config } of configs) { if (config.entryPoint) { bundleConfig.process.args.push(config.entryPoint); @@ -308,7 +310,7 @@ function run(remoteName, pkg) { } layerDirs.reverse(); - prepareBundle(remote, pkg, bundleConfig, layerDirs); + prepareBundle(remote, pkg, bundleConfig, layerDirs, options); if (!remote.socketExists(rialtoSocketPath)) { console.warn('\n\n\x1b[31mRialto socket not available! Playback not supported!\x1b[0m\n\n'); @@ -322,3 +324,69 @@ function run(remoteName, pkg) { } exports.run = run; + +exports.runOptions = { + develop(params, result) { + if (params.options.develop === "") { + Object.assign(result, { + uid: result.uid ?? 0, + gid: result.gid ?? 0, + userns: result.userns ?? false, + }); + return true; + } + return false; + }, + + uid(params, result) { + if (params.options.uid) { + Object.assign(result, { + uid: +params.options.uid, + }); + return true; + } + return false; + }, + + gid(params, result) { + if (params.options.gid) { + Object.assign(result, { + gid: +params.options.gid, + }); + return true; + } + return false; + }, + + userns(params, result) { + const userns = params.options.userns; + let value; + + switch (userns) { + case "true": + value = true; + break; + case "false": + value = false; + break; + default: + return false; + } + + Object.assign(result, { + userns: value, + }); + + return true; + }, + + "clear-storage"(params, result) { + if (params.options["clear-storage"] === "") { + Object.assign(result, { + clearStorage: true, + }); + return true; + } + return false; + }, +}; diff --git a/bolt/src/runtime-config.cjs b/bolt/src/runtime-config.cjs index 1de2c98..8d106bc 100644 --- a/bolt/src/runtime-config.cjs +++ b/bolt/src/runtime-config.cjs @@ -17,13 +17,15 @@ * limitations under the License. */ +const config = require('./config.cjs'); + const template = { "ociVersion": "1.0.2", "process": { "terminal": true, "user": { - "uid": 0, - "gid": 0 + "uid": config.DEFAULT_UID, + "gid": config.DEFAULT_GID, }, "args": [ ], @@ -197,18 +199,8 @@ const template = { }, "linux": { "uidMappings": [ - { - "containerID": 0, - "hostID": 1000, - "size": 1 - } ], "gidMappings": [ - { - "containerID": 0, - "hostID": 1000, - "size": 1 - } ], "namespaces": [ { @@ -255,8 +247,33 @@ const template = { }; -function makeTemplate() { - return template; +function makeTemplate(options) { + const result = JSON.parse(JSON.stringify(template)); + + if (options.uid !== undefined || options.gid !== undefined) { + result.process.user.uid = options.uid ?? config.DEFAULT_UID; + result.process.user.gid = options.gid ?? config.DEFAULT_GID; + } + + if (options.userns ?? true) { + result.linux.namespaces.push({ + type: "user", + }); + + result.linux.uidMappings.push({ + containerID: result.process.user.uid, + hostID: result.process.user.uid, + size: 1, + }); + + result.linux.gidMappings.push({ + containerID: result.process.user.gid, + hostID: result.process.user.gid, + size: 1, + }); + } + + return result; } function addDevice(config, path, type, major, minor) { @@ -286,12 +303,17 @@ function hexToNumber(str) { throw new Error(`Cannot parse ${str} as hex number`); } -function processDevNodeEntry(remote, config, devNode) { +function processDevNodeEntry(remote, config, devNode, groupIds) { try { - const [perm, majorHex, minorHex] = remote.exec(`stat -c '%A:%t:%T' ${devNode}`).trim().split(":"); - const type = perm[0]; - if (type === "c" || type === "b") { - addDevice(config, devNode, type, hexToNumber(majorHex), hexToNumber(minorHex)); + const [perm, majorHex, minorHex, groupName] = remote.exec(`stat -c '%A:%t:%T:%G' ${devNode}`).trim().split(":"); + if (perm.length === 10 && (perm[0] === "c" || perm[0] === "b")) { + addDevice(config, devNode, perm[0], hexToNumber(majorHex), hexToNumber(minorHex)); + if (perm[7] === "r" && perm[8] === "w") { + } else if (groupIds.includes(groupName) && perm[4] === "r" && perm[5] === "w") { + } else { + console.warn(`Changing access rights for ${devNode}! (was ${perm}, group: ${groupName})`); + remote.exec(`chmod a+rw ${devNode}`); + } } else { throw new Error(`not a device (perm: ${perm})`); } @@ -352,7 +374,7 @@ function applyGPUConfig(remote, config, gpuConfig) { const groupIds = gpuConfig?.vendorGpuSupport?.groupIds ?? []; for (let node of devNodes) { - processDevNodeEntry(remote, config, node); + processDevNodeEntry(remote, config, node, groupIds); } for (let file of files) { processFileEntry(config, file);