Skip to content

Commit 8672859

Browse files
committed
NC | generate_entropy Using lsblk Command
Create the file entropy_utils.js and separate functions: 1. Move the generate_entropy with changes: - change the condition if (entropy_avail < 512) to if (entropy_avail >= 512) return; (the constant 512 moved to the config) to avoid additional nested layer. - Add the if (loop_cond()) break; to avoid redundant disk read (see comment). - Change the printing to not have the error stuck (see comment). 2. get_block_device_disk_info which is based on get_block_device_sizes from os_utils - I couldn't add it there as it would create a circular dependency 3. pick_a_disk instead of using a static array, and get the size with blockdev --getsize64 we would use lsblk command (which is implemented in blockutils.getBlockInfo) and create a priority list and minimum size of the disk. 4. generate_entropy_from_disk (copied from original code with a change - in skip see comment about it). Signed-off-by: shirady <[email protected]>
1 parent 04307d7 commit 8672859

File tree

4 files changed

+223
-43
lines changed

4 files changed

+223
-43
lines changed

config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,9 @@ config.NC_DISABLE_HEALTH_ACCESS_CHECK = false;
10091009
config.NC_DISABLE_POSIX_MODE_ACCESS_CHECK = true;
10101010
config.NC_DISABLE_SCHEMA_CHECK = false;
10111011

1012+
config.ENTROPY_DISK_SIZE_THRESHOLD = 100 * 1024 * 1024;
1013+
config.ENTROPY_MIN_THRESHOLD = 512;
1014+
10121015
////////// NC LIFECYLE //////////
10131016

10141017
config.NC_LIFECYCLE_LOGS_DIR = '/var/log/noobaa/lifecycle';
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* Copyright (C) 2025 NooBaa */
2+
'use strict';
3+
4+
const config = require('../../../../config');
5+
const entropy_utils = require('../../../util/entropy_utils');
6+
7+
describe('entropy_utils', () => {
8+
9+
describe('get_block_device_disk_info', () => {
10+
11+
it('(on Linux) should return array of disk devices that their names starts with ' +
12+
'/dev/, and their size it a number in bytes ' +
13+
'(else) empty array', async () => {
14+
const res = await entropy_utils.get_block_device_disk_info();
15+
expect(Array.isArray(res)).toBe(true);
16+
const is_linux = process.platform === 'linux';
17+
console.log('is_linux', is_linux, ' entropy_utils.get_block_device_disk_info res', res);
18+
if (is_linux) {
19+
res.forEach(item => {
20+
expect(item.name.startsWith('/dev/')).toBe(true);
21+
expect(typeof(item.size)).toBe('number');
22+
});
23+
} else {
24+
expect(res.length).toBe(0);
25+
}
26+
});
27+
28+
});
29+
30+
describe('pick_a_disk', () => {
31+
32+
it('should return sd disk with size greater than 100 MiB', async () => {
33+
const mock_arr = [
34+
{ name: '/dev/sda', size: config.ENTROPY_DISK_SIZE_THRESHOLD + 1 },
35+
{ name: '/dev/vda', size: 5 } // size too small
36+
];
37+
const res = await entropy_utils.pick_a_disk(mock_arr);
38+
expect(res.name.startsWith('/dev/sd')).toBe(true);
39+
expect(res.size).toBeGreaterThan(config.ENTROPY_DISK_SIZE_THRESHOLD);
40+
});
41+
42+
it('should return vd disk with size greater than 100 MiB', async () => {
43+
const mock_arr = [
44+
{name: '/dev/sda', size: 5 }, // size too small
45+
{ name: '/dev/vda', size: config.ENTROPY_DISK_SIZE_THRESHOLD + 1 }];
46+
const res = await entropy_utils.pick_a_disk(mock_arr);
47+
expect(res.name.startsWith('/dev/vd')).toBe(true);
48+
expect(res.size).toBeGreaterThan(config.ENTROPY_DISK_SIZE_THRESHOLD);
49+
});
50+
51+
it('should return nvme disk with size greater than 100 MiB', async () => {
52+
const mock_arr = [
53+
{ name: '/dev/nvme1n1', size: config.ENTROPY_DISK_SIZE_THRESHOLD + 1 },
54+
{ name: '/dev/some_disk', size: config.ENTROPY_DISK_SIZE_THRESHOLD + 1 } // name is not in whitelist
55+
];
56+
const res = await entropy_utils.pick_a_disk(mock_arr);
57+
expect(res.name.startsWith('/dev/nvme')).toBe(true);
58+
expect(res.size).toBeGreaterThan(config.ENTROPY_DISK_SIZE_THRESHOLD);
59+
});
60+
61+
it('should return undefined', async () => {
62+
const mock_arr = [
63+
{ name: '/dev/sda', size: config.ENTROPY_DISK_SIZE_THRESHOLD - 1 }, // size too small
64+
{ name: '/dev/vda', size: config.ENTROPY_DISK_SIZE_THRESHOLD - 1 } // size too small
65+
];
66+
const res = await entropy_utils.pick_a_disk(mock_arr);
67+
expect(res).toBeUndefined();
68+
});
69+
70+
it('should return vd disk with size greater than 100 MiB', async () => {
71+
const mock_arr = [
72+
{ name: '/dev/vda', size: config.ENTROPY_DISK_SIZE_THRESHOLD + 1 },
73+
{ name: '/dev/nvme1n1', size: config.ENTROPY_DISK_SIZE_THRESHOLD + 1 }
74+
];
75+
const res = await entropy_utils.pick_a_disk(mock_arr);
76+
expect(res.name.startsWith('/dev/vda')).toBe(true); // vd in higher priority than nvme
77+
expect(res.size).toBeGreaterThan(config.ENTROPY_DISK_SIZE_THRESHOLD);
78+
});
79+
});
80+
});

src/util/entropy_utils.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/* Copyright (C) 2025 NooBaa */
2+
'use strict';
3+
4+
const fs = require('fs');
5+
const util = require('util');
6+
const chance = require('chance')();
7+
const child_process = require('child_process');
8+
const config = require('../../config');
9+
const blockutils = require('linux-blockutils');
10+
11+
const DEVICE_TYPE_DISK = 'disk';
12+
13+
const async_delay = util.promisify(setTimeout);
14+
const async_exec = util.promisify(child_process.exec);
15+
const get_block_info_async = util.promisify(blockutils.getBlockInfo);
16+
17+
/**
18+
* generate_entropy will create randomness by changing the MD5
19+
* using information from the device (disk)
20+
* it will run as long as the callback it true
21+
* @param {() => Boolean} loop_cond
22+
*/
23+
async function generate_entropy(loop_cond) {
24+
if (process.platform !== 'linux' || process.env.container === 'docker') return;
25+
while (loop_cond()) {
26+
try {
27+
await async_delay(1000);
28+
29+
// most likely we will read from the disk for no reason at all as the caller
30+
// already finished initializing its randomness
31+
if (loop_cond()) break;
32+
33+
const ENTROPY_AVAIL_PATH = '/proc/sys/kernel/random/entropy_avail';
34+
const entropy_avail = parseInt(await fs.promises.readFile(ENTROPY_AVAIL_PATH, 'utf8'), 10);
35+
console.log(`generate_entropy: entropy_avail ${entropy_avail}`);
36+
if (entropy_avail >= config.ENTROPY_MIN_THRESHOLD) return;
37+
const available_disks = await get_block_device_disk_info();
38+
const disk_details = pick_a_disk(available_disks);
39+
if (disk_details) {
40+
await generate_entropy_from_disk(disk_details.name, disk_details.size);
41+
} else {
42+
throw new Error('No disk candidates found');
43+
}
44+
} catch (err) {
45+
// we intentionally don't print the error
46+
// as console.err/warm and the error with stack trace
47+
// as the generate_entropy is our best effort to generate entropy
48+
// until Linux will finally do it
49+
console.log('generate_entropy: retrying');
50+
}
51+
}
52+
}
53+
54+
/**
55+
* get_block_device_disk_info
56+
* (on Linux) will return array of disk devices
57+
* that their names starts with /dev/
58+
* and their size it a number in bytes
59+
* (else) will return an empty array
60+
* Note: under the hood it uses blockutils.getBlockInfo which calls lsblk command
61+
* we used the option onlyStandard for parsing "disk" and "part" entries
62+
* reference: https://github.com/mw-white/node-linux-blockutils
63+
* @returns {Promise<object[]>}
64+
*/
65+
async function get_block_device_disk_info() {
66+
const is_linux = process.platform === 'linux';
67+
if (!is_linux) return [];
68+
69+
const block_devices = await get_block_info_async({ onlyStandard: true });
70+
if (!block_devices) return [];
71+
72+
const available_disks = [];
73+
for (const block_device of block_devices) {
74+
if (block_device.TYPE === DEVICE_TYPE_DISK) {
75+
available_disks.push({
76+
name: `/dev/${block_device.NAME}`,
77+
size: Number(block_device.SIZE),
78+
});
79+
}
80+
}
81+
return available_disks;
82+
}
83+
84+
/**
85+
* pick_a_disk will pick a disk that:
86+
* - The disk name is in the disk_order_by_priority array - the lower index in the array the higher priority
87+
* - Its size is higher than config.ENTROPY_DISK_SIZE_THRESHOLD
88+
*
89+
* The item that is returned is an object with properties: name, size and priority
90+
* (we could delete the priority property as it is used only inside this function)
91+
*
92+
* if none matches then it would return undefined
93+
* @param {object[]} available_disks
94+
* @returns {object | undefined}
95+
*/
96+
function pick_a_disk(available_disks) {
97+
// the disk_order_by_priority array is the whitelist and also sets the priority
98+
// (0 index highest priority, n-1 index lowest priority)
99+
// note: we decided on the order according to previous implantation we had in the past
100+
// as it added in patches we might want to reconsider it
101+
const disk_order_by_priority = ['/dev/sd', '/dev/vd', '/dev/xvd', 'dev/dasd', '/dev/nvme'];
102+
const priority_disk_array = [];
103+
for (const disk of available_disks) {
104+
if (disk.size > config.ENTROPY_DISK_SIZE_THRESHOLD) {
105+
for (let index = 0; index < disk_order_by_priority.length; index++) {
106+
const prefix = disk_order_by_priority[index];
107+
if (disk.name.startsWith(prefix)) {
108+
disk.priority = index;
109+
priority_disk_array.push(disk);
110+
}
111+
}
112+
}
113+
}
114+
// sort the array based on the priority
115+
const sorted_priority_disk_array = priority_disk_array.sort((item1, item2) => item1.priority - item2.priority);
116+
const first_element = sorted_priority_disk_array.length > 0 ? sorted_priority_disk_array[0] : undefined;
117+
return first_element;
118+
}
119+
120+
/**
121+
* generate_entropy_from_disk will execute dd command which pipes to md5sum
122+
* in order to get the MD5 hash to change
123+
* @param {string} disk_name
124+
* @param {number} disk_size
125+
*/
126+
async function generate_entropy_from_disk(disk_name, disk_size) {
127+
const bs = 1024 * 1024;
128+
const count = 32;
129+
const disk_size_in_blocks = disk_size / bs;
130+
const skip = chance.integer({ min: 0, max: disk_size_in_blocks - 1}); // reduce 1 from the max because the range is inclusive in chance.integer()
131+
console.log(`generate_entropy_from_disk: adding entropy: dd if=${disk_name} bs=${bs} count=${count} skip=${skip} | md5sum`);
132+
await async_exec(`dd if=${disk_name} bs=${bs} count=${count} skip=${skip} | md5sum`);
133+
}
134+
135+
exports.generate_entropy = generate_entropy;
136+
exports.get_block_device_disk_info = get_block_device_disk_info;
137+
exports.pick_a_disk = pick_a_disk;
138+
exports.generate_entropy_from_disk = generate_entropy_from_disk;

src/util/nb_native.js

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ const _ = require('lodash');
55
const fs = require('fs');
66
const util = require('util');
77
const events = require('events');
8-
const chance = require('chance')();
98
const bindings = require('bindings');
10-
const child_process = require('child_process');
119
const config = require('../../config');
10+
const entropy_utils = require('./entropy_utils');
1211

13-
const async_exec = util.promisify(child_process.exec);
1412
const async_delay = util.promisify(setTimeout);
1513

1614
let nb_native_napi;
@@ -51,7 +49,7 @@ async function init_rand_seed() {
5149
console.log('init_rand_seed: starting ...');
5250
}
5351
let still_reading = true;
54-
const promise = generate_entropy(() => still_reading);
52+
const promise = entropy_utils.generate_entropy(() => still_reading);
5553

5654
const seed = await read_rand_seed(32);
5755
if (seed) {
@@ -107,43 +105,4 @@ async function read_rand_seed(seed_bytes) {
107105
return buf;
108106
}
109107

110-
async function generate_entropy(loop_cond) {
111-
if (process.platform !== 'linux' || process.env.container === 'docker') return;
112-
while (loop_cond()) {
113-
try {
114-
await async_delay(1000);
115-
const ENTROPY_AVAIL_PATH = '/proc/sys/kernel/random/entropy_avail';
116-
const entropy_avail = parseInt(await fs.promises.readFile(ENTROPY_AVAIL_PATH, 'utf8'), 10);
117-
console.log(`generate_entropy: entropy_avail ${entropy_avail}`);
118-
if (entropy_avail < 512) {
119-
const bs = 1024 * 1024;
120-
const count = 32;
121-
let disk;
122-
let disk_size;
123-
// this is as a temporary and partial solution -
124-
// adding the NVMe disk with namespace
125-
for (disk of ['/dev/sda', '/dev/vda', '/dev/xvda', '/dev/dasda', '/dev/nvme0n1', '/dev/nvme1n1']) {
126-
try {
127-
const res = await async_exec(`blockdev --getsize64 ${disk}`);
128-
disk_size = res.stdout;
129-
break;
130-
} catch (err) {
131-
//continue to next candidate
132-
}
133-
}
134-
if (disk_size) {
135-
const disk_size_in_blocks = parseInt(disk_size, 10) / bs;
136-
const skip = chance.integer({ min: 0, max: disk_size_in_blocks });
137-
console.log(`generate_entropy: adding entropy: dd if=${disk} bs=${bs} count=${count} skip=${skip} | md5sum`);
138-
await async_exec(`dd if=${disk} bs=${bs} count=${count} skip=${skip} | md5sum`);
139-
} else {
140-
throw new Error('No disk candidates found');
141-
}
142-
}
143-
} catch (err) {
144-
console.log('generate_entropy: error', err);
145-
}
146-
}
147-
}
148-
149108
module.exports = nb_native;

0 commit comments

Comments
 (0)