From 2b93cc44573be3abbb4850e28b2deade01e303ba Mon Sep 17 00:00:00 2001 From: Ashton Kinslow Date: Thu, 12 Jun 2025 23:05:28 -0500 Subject: [PATCH] Add basic tests for node-init utilities --- README.md | 8 +++- bin/node-init | 18 ++++++++ package.json | 6 ++- src/node-init/drivers/cifs.js | 7 ++++ src/node-init/drivers/iscsi.js | 26 ++++++++++++ src/node-init/drivers/nfs.js | 7 ++++ src/node-init/drivers/nvmeof.js | 15 +++++++ src/node-init/index.js | 22 ++++++++++ src/node-init/utils.js | 59 ++++++++++++++++++++++++++ test/node-init-utils.test.js | 74 +++++++++++++++++++++++++++++++++ 10 files changed, 240 insertions(+), 2 deletions(-) create mode 100755 bin/node-init create mode 100644 src/node-init/drivers/cifs.js create mode 100644 src/node-init/drivers/iscsi.js create mode 100644 src/node-init/drivers/nfs.js create mode 100644 src/node-init/drivers/nvmeof.js create mode 100644 src/node-init/index.js create mode 100644 src/node-init/utils.js create mode 100644 test/node-init-utils.test.js diff --git a/README.md b/README.md index 79992b5..463064e 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,13 @@ Predominantly 3 things are needed: ## Node Prep -You should install/configure the requirements for both nfs and iscsi. +You should install/configure the requirements for both nfs and iscsi. A helper +command `node-init` is provided to automate this process. Invoke it with one or +more driver names and it will run the appropriate initialization steps: + +``` +node-init --driver iscsi --driver nfs +``` ### cifs diff --git a/bin/node-init b/bin/node-init new file mode 100755 index 0000000..a9f9e41 --- /dev/null +++ b/bin/node-init @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +const { run } = require('../src/node-init'); +const args = require('yargs') + .scriptName('node-init') + .usage('$0 --driver [--driver ...]') + .option('driver', { + alias: 'd', + type: 'array', + demandOption: true, + describe: 'CSI driver name to initialize', + }) + .help().argv; + +run(args.driver).catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/package.json b/package.json index 099fd4e..69a42d7 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,12 @@ "version": "1.9.0", "description": "kubernetes csi driver framework", "main": "bin/democratic-csi", + "bin": { + "democratic-csi": "bin/democratic-csi", + "node-init": "bin/node-init" + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node --test", "start": "./bin/democratic-csi" }, "author": "Travis Glenn Hansen ", diff --git a/src/node-init/drivers/cifs.js b/src/node-init/drivers/cifs.js new file mode 100644 index 0000000..c49b6b3 --- /dev/null +++ b/src/node-init/drivers/cifs.js @@ -0,0 +1,7 @@ +const { installPackages } = require('../utils'); + +async function run() { + installPackages({ apt: ['cifs-utils'], yum: ['cifs-utils'] }); +} + +module.exports = { run }; diff --git a/src/node-init/drivers/iscsi.js b/src/node-init/drivers/iscsi.js new file mode 100644 index 0000000..b64719f --- /dev/null +++ b/src/node-init/drivers/iscsi.js @@ -0,0 +1,26 @@ +const { installPackages, enableService, startService, runCommand, commandExists } = require('../utils'); +const fs = require('fs'); + +async function run() { + installPackages({ + apt: ['open-iscsi','lsscsi','sg3-utils','multipath-tools','scsitools'], + yum: ['lsscsi','iscsi-initiator-utils','sg3_utils','device-mapper-multipath'] + }); + + if (!fs.existsSync('/etc/multipath.conf')) { + // configuration may be needed but is skipped if file is absent + } + + if (commandExists('mpathconf')) { + try { + runCommand('mpathconf', ['--enable','--with_multipathd','y']); + } catch (e) {} + } + + ;['iscsid','multipathd','iscsi','open-iscsi'].forEach(s => { + enableService(s); + startService(s); + }); +} + +module.exports = { run }; diff --git a/src/node-init/drivers/nfs.js b/src/node-init/drivers/nfs.js new file mode 100644 index 0000000..cf4940c --- /dev/null +++ b/src/node-init/drivers/nfs.js @@ -0,0 +1,7 @@ +const { installPackages } = require('../utils'); + +async function run() { + installPackages({ apt: ['nfs-common'], yum: ['nfs-utils'] }); +} + +module.exports = { run }; diff --git a/src/node-init/drivers/nvmeof.js b/src/node-init/drivers/nvmeof.js new file mode 100644 index 0000000..3942a35 --- /dev/null +++ b/src/node-init/drivers/nvmeof.js @@ -0,0 +1,15 @@ +const { installPackages, loadModule } = require('../utils'); +const fs = require('fs'); + +async function run() { + installPackages({ apt: ['nvme-cli','linux-generic'], yum: ['nvme-cli'] }); + + const config = '/etc/modules-load.d/nvme.conf'; + if (!fs.existsSync(config)) { + fs.writeFileSync(config, 'nvme\nnvme-tcp\nnvme-fc\nnvme-rdma\n'); + } + + ['nvme','nvme-tcp','nvme-fc','nvme-rdma'].forEach(loadModule); +} + +module.exports = { run }; diff --git a/src/node-init/index.js b/src/node-init/index.js new file mode 100644 index 0000000..f3464a4 --- /dev/null +++ b/src/node-init/index.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const path = require('path'); + +async function run(drivers) { + for (const name of drivers) { + const file = path.join(__dirname, 'drivers', `${name}.js`); + if (!fs.existsSync(file)) { + console.log(`no node init script for driver: ${name}`); + continue; + } + const mod = require(file); + try { + if (typeof mod.run === 'function') { + await mod.run(); + } + } catch (e) { + console.error(`driver ${name} failed: ${e.toString()}`); + } + } +} + +module.exports = { run }; diff --git a/src/node-init/utils.js b/src/node-init/utils.js new file mode 100644 index 0000000..d78e60e --- /dev/null +++ b/src/node-init/utils.js @@ -0,0 +1,59 @@ +const { spawnSync } = require('child_process'); +const fs = require('fs'); + +function commandExists(cmd) { + const res = spawnSync('which', [cmd]); + return res.status === 0; +} + +function runCommand(cmd, args) { + console.log(`executing: ${cmd} ${args.join(' ')}`); + const res = spawnSync(cmd, args, { stdio: 'inherit' }); + if (res.error) throw res.error; + if (res.status !== 0) throw new Error(`${cmd} exited with code ${res.status}`); +} + +function installPackages(pkgs) { + if (!pkgs) return; + if (commandExists('apt-get')) { + (pkgs.apt || []).forEach((p) => { + if (spawnSync('dpkg', ['-s', p], { stdio: 'ignore' }).status !== 0) { + runCommand('apt-get', ['install', '-y', p]); + } + }); + } else if (commandExists('yum')) { + (pkgs.yum || []).forEach((p) => { + if (spawnSync('rpm', ['-q', p], { stdio: 'ignore' }).status !== 0) { + runCommand('yum', ['install', '-y', p]); + } + }); + } else { + console.log('no supported package manager found'); + } +} + +function enableService(name) { + if (commandExists('systemctl')) { + if (spawnSync('systemctl', ['is-enabled', name], { stdio: 'ignore' }).status !== 0) { + runCommand('systemctl', ['enable', name]); + } + } +} + +function startService(name) { + if (commandExists('systemctl')) { + if (spawnSync('systemctl', ['is-active', name], { stdio: 'ignore' }).status !== 0) { + runCommand('systemctl', ['start', name]); + } + } +} + +function loadModule(name) { + if (!fs.existsSync(`/sys/module/${name}`)) { + if (commandExists('modprobe')) { + runCommand('modprobe', [name]); + } + } +} + +module.exports = { commandExists, runCommand, installPackages, enableService, startService, loadModule }; diff --git a/test/node-init-utils.test.js b/test/node-init-utils.test.js new file mode 100644 index 0000000..67d8f6f --- /dev/null +++ b/test/node-init-utils.test.js @@ -0,0 +1,74 @@ +const test = require('node:test'); +const assert = require('node:assert'); + +function loadUtils(mockSpawn, mockExists) { + const cp = require('child_process'); + const fs = require('fs'); + cp.spawnSync = mockSpawn; + if (mockExists) fs.existsSync = mockExists; + delete require.cache[require.resolve('../src/node-init/utils')]; + return require('../src/node-init/utils'); +} + +test('commandExists true', () => { + const calls = []; + const utils = loadUtils((...args) => { calls.push(args); return {status:0}; }); + assert.strictEqual(utils.commandExists('ls'), true); + assert.deepStrictEqual(calls[0], ['which', ['ls']]); +}); + +test('commandExists false', () => { + const utils = loadUtils(() => ({status:1})); + assert.strictEqual(utils.commandExists('ls'), false); +}); + +test('runCommand success', () => { + const calls = []; + const utils = loadUtils((...args) => { calls.push(args); return {status:0}; }); + assert.doesNotThrow(() => utils.runCommand('cmd', ['arg'])); + assert.deepStrictEqual(calls[0], ['cmd', ['arg'], {stdio:'inherit'}]); +}); + +test('runCommand failure', () => { + const utils = loadUtils(() => ({status:1})); + assert.throws(() => utils.runCommand('cmd', [])); +}); + +test('installPackages apt missing', () => { + const calls = []; + const responses = [ + {status:0}, // which apt-get + {status:1}, // dpkg -s + {status:0}, // apt-get install + ]; + const mock = (...args) => { calls.push(args); return responses.shift(); }; + const utils = loadUtils(mock); + utils.installPackages({apt:['pkg']}); + assert.deepStrictEqual(calls[2], ['apt-get', ['install','-y','pkg'], {stdio:'inherit'}]); +}); + +test('installPackages apt installed', () => { + const calls = []; + const responses = [ + {status:0}, // which apt-get + {status:0}, // dpkg -s (installed) + ]; + const mock = (...args) => { calls.push(args); return responses.shift(); }; + const utils = loadUtils(mock); + utils.installPackages({apt:['pkg']}); + assert.strictEqual(calls.length, 2); // no install call +}); + +test('installPackages yum missing', () => { + const calls = []; + const responses = [ + {status:1}, // which apt-get + {status:0}, // which yum + {status:1}, // rpm -q + {status:0}, // yum install + ]; + const mock = (...args) => { calls.push(args); return responses.shift(); }; + const utils = loadUtils(mock); + utils.installPackages({yum:['pkg']}); + assert.deepStrictEqual(calls[3], ['yum', ['install','-y','pkg'], {stdio:'inherit'}]); +});