underpost 3.2.9 → 3.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npmpkg.ci.yml +1 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/extensions.json +9 -9
- package/.vscode/settings.json +20 -4
- package/CHANGELOG.md +195 -1
- package/CLI-HELP.md +92 -23
- package/README.md +38 -9
- package/bin/build.js +27 -7
- package/bin/build.template.js +187 -0
- package/bin/deploy.js +12 -2
- package/bin/index.js +2 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -7
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/lxd/lxd-admin-profile.yaml +12 -3
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/manifests/mongodb-4.4/headless-service.yaml +10 -0
- package/manifests/mongodb-4.4/kustomization.yaml +3 -1
- package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
- package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
- package/manifests/mongodb-4.4/statefulset.yaml +79 -0
- package/manifests/mongodb-4.4/storage-class.yaml +9 -0
- package/manifests/valkey/statefulset.yaml +1 -1
- package/manifests/valkey/valkey-nodeport.yaml +17 -0
- package/package.json +27 -12
- package/scripts/ipxe-setup.sh +52 -49
- package/scripts/k3s-node-setup.sh +81 -46
- package/scripts/lxd-vm-setup.sh +193 -8
- package/scripts/maas-nat-firewalld.sh +145 -0
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +132 -101
- package/src/cli/cluster.js +700 -232
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +216 -137
- package/src/cli/fs.js +13 -3
- package/src/cli/index.js +80 -15
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +1099 -223
- package/src/cli/monitor.js +9 -3
- package/src/cli/release.js +334 -140
- package/src/cli/repository.js +68 -23
- package/src/cli/run.js +191 -47
- package/src/cli/secrets.js +11 -2
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +5 -0
- package/src/client/components/core/ClientEvents.js +76 -0
- package/src/client/components/core/EventBus.js +4 -0
- package/src/client/components/core/Modal.js +82 -41
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Worker.js +162 -363
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +115 -15
- package/src/db/mariadb/MariaDB.js +2 -1
- package/src/db/mongo/MongoBootstrap.js +657 -0
- package/src/db/mongo/MongooseDB.js +129 -21
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +81 -79
- package/src/server/process.js +180 -19
- package/src/server/proxy.js +9 -2
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +16 -4
- package/src/server/valkey.js +2 -0
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/bin/file.js +0 -202
- package/bin/vs.js +0 -74
- package/bin/zed.js +0 -84
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /package/src/client/ssr/{pages → views}/Test.js +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User service module for handling user account operations.
|
|
3
|
+
* Provides REST API handlers for authentication, registration, profile management,
|
|
4
|
+
* email verification, password recovery, and guest user lifecycle management.
|
|
5
|
+
*
|
|
6
|
+
* @module src/api/user/user.service.js
|
|
7
|
+
* @namespace UserService
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
import { loggerFactory } from '../../server/logger.js';
|
|
2
11
|
import { DataQuery } from '../../server/data-query.js';
|
|
3
12
|
import {
|
|
@@ -15,21 +24,40 @@ import { MailerProvider } from '../../mailer/MailerProvider.js';
|
|
|
15
24
|
import { CoreWsEmitter } from '../../ws/core/core.ws.emit.js';
|
|
16
25
|
import { CoreWsMailerChannel } from '../../ws/core/channels/core.ws.mailer.js';
|
|
17
26
|
import validator from 'validator';
|
|
18
|
-
import {
|
|
27
|
+
import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
|
|
19
28
|
import { FileFactory, FileCleanup } from '../file/file.service.js';
|
|
20
29
|
import { UserDto } from './user.model.js';
|
|
21
30
|
import { timer } from '../../client/components/core/CommonJs.js';
|
|
22
31
|
import { GuestService } from './guest.service.js';
|
|
32
|
+
import { resolveHostKeyContext } from '../../server/conf.js';
|
|
23
33
|
|
|
24
34
|
const logger = loggerFactory(import.meta);
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
/**
|
|
37
|
+
* User Service for handling REST API user operations.
|
|
38
|
+
* Manages authentication, registration, profile CRUD, email verification,
|
|
39
|
+
* password recovery, session management, and guest user lifecycle.
|
|
40
|
+
* @namespace UserService
|
|
41
|
+
*/
|
|
42
|
+
class UserService {
|
|
43
|
+
/**
|
|
44
|
+
* POST - Create or authenticate users.
|
|
45
|
+
* Supports authentication, guest account creation, email verification,
|
|
46
|
+
* and password recovery email sending.
|
|
47
|
+
* @async
|
|
48
|
+
* @function post
|
|
49
|
+
* @memberof UserService
|
|
50
|
+
* @param {Object} req - Express request object.
|
|
51
|
+
* @param {Object} res - Express response object.
|
|
52
|
+
* @param {Object} options - Request options containing host and path.
|
|
53
|
+
* @returns {Promise<Object>} User data with auth token, or status message.
|
|
54
|
+
* @throws {Error} If authentication fails, email is invalid, or email send error.
|
|
55
|
+
*/
|
|
56
|
+
static post = async (req, res, options) => {
|
|
31
57
|
/** @type {import('../file/file.model.js').FileModel} */
|
|
32
|
-
const File =
|
|
58
|
+
const File = DataBaseProviderService.getModel('File', options);
|
|
59
|
+
/** @type {import('./user.model.js').UserModel} */
|
|
60
|
+
const User = DataBaseProviderService.getModel('User', options);
|
|
33
61
|
|
|
34
62
|
if (req.params.id === 'recover-verify-email') {
|
|
35
63
|
const user = await User.findOne({
|
|
@@ -44,11 +72,10 @@ const UserService = {
|
|
|
44
72
|
|
|
45
73
|
const token = jwtSign({ email: req.body.email }, options, 15);
|
|
46
74
|
const payloadToken = jwtSign({ email: req.body.email }, options, 15);
|
|
47
|
-
const id =
|
|
75
|
+
const id = resolveHostKeyContext(options);
|
|
48
76
|
const translate = MailerProvider.instance[id].translateTemplates.recoverEmail;
|
|
49
|
-
const recoverUrl = `${process.env.NODE_ENV === 'development' ? 'http://' : 'https://'}${req.body.hostname}${
|
|
50
|
-
|
|
51
|
-
}recover?payload=${payloadToken}`;
|
|
77
|
+
const recoverUrl = `${process.env.NODE_ENV === 'development' ? 'http://' : 'https://'}${req.body.hostname}${req.body.proxyPath
|
|
78
|
+
}recover?payload=${payloadToken}`;
|
|
52
79
|
const sendResult = await MailerProvider.send({
|
|
53
80
|
id,
|
|
54
81
|
sendOptions: {
|
|
@@ -81,7 +108,7 @@ const UserService = {
|
|
|
81
108
|
if (!validator.isEmail(req.body.email)) throw { message: 'invalid email' };
|
|
82
109
|
|
|
83
110
|
const token = jwtSign({ email: req.body.email }, options, 15);
|
|
84
|
-
const id =
|
|
111
|
+
const id = resolveHostKeyContext(options);
|
|
85
112
|
const user = await User.findById(req.auth.user._id);
|
|
86
113
|
|
|
87
114
|
if (user.emailConfirmed) throw new Error('email already confirmed');
|
|
@@ -130,10 +157,9 @@ const UserService = {
|
|
|
130
157
|
});
|
|
131
158
|
const getMinutesRemaining = () => (-1 * user.failedLoginAttempts - new Date().getTime()) / (1000 * 60);
|
|
132
159
|
const accountLocketMessage = () =>
|
|
133
|
-
`Account locked. Please try again in: ${
|
|
134
|
-
getMinutesRemaining()
|
|
135
|
-
|
|
136
|
-
: `${getMinutesRemaining().toFixed(0)} min`
|
|
160
|
+
`Account locked. Please try again in: ${getMinutesRemaining() < 1
|
|
161
|
+
? `${(getMinutesRemaining() * 60).toFixed(0)} s`
|
|
162
|
+
: `${getMinutesRemaining().toFixed(0)} min`
|
|
137
163
|
}.`;
|
|
138
164
|
|
|
139
165
|
if (user) {
|
|
@@ -231,13 +257,27 @@ const UserService = {
|
|
|
231
257
|
default:
|
|
232
258
|
return await createUserAndSession(req, res, User, options);
|
|
233
259
|
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* GET - Retrieve user data.
|
|
264
|
+
* Supports public profile lookup by username, asset retrieval,
|
|
265
|
+
* email lookup, password recovery flow, email verification,
|
|
266
|
+
* admin user listing, authentication refresh, and profile retrieval.
|
|
267
|
+
* @async
|
|
268
|
+
* @function get
|
|
269
|
+
* @memberof UserService
|
|
270
|
+
* @param {Object} req - Express request object.
|
|
271
|
+
* @param {Object} res - Express response object.
|
|
272
|
+
* @param {Object} options - Request options containing host and path.
|
|
273
|
+
* @returns {Promise<Object>} User data with optional session token.
|
|
274
|
+
* @throws {Error} If user not found, profile is private, or token invalid.
|
|
275
|
+
*/
|
|
276
|
+
static get = async (req, res, options) => {
|
|
239
277
|
/** @type {import('../file/file.model.js').FileModel} */
|
|
240
|
-
const File =
|
|
278
|
+
const File = DataBaseProviderService.getModel('File', options);
|
|
279
|
+
/** @type {import('./user.model.js').UserModel} */
|
|
280
|
+
const User = DataBaseProviderService.getModel('User', options);
|
|
241
281
|
|
|
242
282
|
if (req.path.startsWith('/u/')) {
|
|
243
283
|
// First lookup user by username
|
|
@@ -309,7 +349,7 @@ const UserService = {
|
|
|
309
349
|
{
|
|
310
350
|
const user = await User.findByIdAndUpdate(_id, { emailConfirmed: true }, { runValidators: true });
|
|
311
351
|
}
|
|
312
|
-
const userWsId = CoreWsMailerChannel.getUserWsId(
|
|
352
|
+
const userWsId = CoreWsMailerChannel.getUserWsId(resolveHostKeyContext(options), user._id.toString());
|
|
313
353
|
if (userWsId && CoreWsMailerChannel.client[userWsId]) {
|
|
314
354
|
CoreWsEmitter.emit(CoreWsMailerChannel.channel, CoreWsMailerChannel.client[userWsId], {
|
|
315
355
|
status: 'email-confirmed',
|
|
@@ -384,10 +424,23 @@ const UserService = {
|
|
|
384
424
|
}
|
|
385
425
|
}
|
|
386
426
|
}
|
|
387
|
-
}
|
|
388
|
-
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* DELETE - Remove user accounts or log out sessions.
|
|
431
|
+
* Supports admin bulk deletion, user self-deletion, and session logout.
|
|
432
|
+
* @async
|
|
433
|
+
* @function delete
|
|
434
|
+
* @memberof UserService
|
|
435
|
+
* @param {Object} req - Express request object.
|
|
436
|
+
* @param {Object} res - Express response object.
|
|
437
|
+
* @param {Object} options - Request options containing host and path.
|
|
438
|
+
* @returns {Promise<Object>} Deleted user data or logout status message.
|
|
439
|
+
* @throws {Error} If logout fails, user not found, or token invalid.
|
|
440
|
+
*/
|
|
441
|
+
static delete = async (req, res, options) => {
|
|
389
442
|
/** @type {import('./user.model.js').UserModel} */
|
|
390
|
-
const User =
|
|
443
|
+
const User = DataBaseProviderService.getModel('User', options);
|
|
391
444
|
|
|
392
445
|
if (req.params.id === 'logout') {
|
|
393
446
|
const result = await logoutSession(User, req, res);
|
|
@@ -417,13 +470,25 @@ const UserService = {
|
|
|
417
470
|
}
|
|
418
471
|
}
|
|
419
472
|
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* PUT - Update user data.
|
|
477
|
+
* Supports profile image upload, password recovery, and profile field updates.
|
|
478
|
+
* @async
|
|
479
|
+
* @function put
|
|
480
|
+
* @memberof UserService
|
|
481
|
+
* @param {Object} req - Express request object.
|
|
482
|
+
* @param {Object} res - Express response object.
|
|
483
|
+
* @param {Object} options - Request options containing host and path.
|
|
484
|
+
* @returns {Promise<Object>} Updated user data.
|
|
485
|
+
* @throws {Error} If user not found, token invalid, or invalid file.
|
|
486
|
+
*/
|
|
487
|
+
static put = async (req, res, options) => {
|
|
425
488
|
/** @type {import('../file/file.model.js').FileModel} */
|
|
426
|
-
const File =
|
|
489
|
+
const File = DataBaseProviderService.getModel('File', options);
|
|
490
|
+
/** @type {import('./user.model.js').UserModel} */
|
|
491
|
+
const User = DataBaseProviderService.getModel('User', options);
|
|
427
492
|
|
|
428
493
|
// req.path | req.baseUrl
|
|
429
494
|
|
|
@@ -511,7 +576,7 @@ const UserService = {
|
|
|
511
576
|
}
|
|
512
577
|
}
|
|
513
578
|
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
516
581
|
|
|
517
|
-
export { UserService };
|
|
582
|
+
export { UserService };
|
package/src/cli/baremetal.js
CHANGED
|
@@ -27,6 +27,13 @@ const logger = loggerFactory(import.meta);
|
|
|
27
27
|
* and system provisioning for different architectures.
|
|
28
28
|
*/
|
|
29
29
|
class UnderpostBaremetal {
|
|
30
|
+
// NFSv3 RPC ports. Single source of truth shared by the firewall/export setup
|
|
31
|
+
// (rebuildNfsServer) and the kernel `nfsroot=` mount options so the client mount and the
|
|
32
|
+
// opened firewall ports always agree.
|
|
33
|
+
// rpc.statd rejects identical listen and outgoing ports (exit 255 "Listening and outgoing ports cannot be the same!").
|
|
34
|
+
// statd=32765 (listen), statdOutgoing=32766 (SM_NOTIFY source port) is the standard split.
|
|
35
|
+
static NFS_V3_PORTS = { mountd: 20048, statd: 32765, statdOutgoing: 32766, lockd: 32803 };
|
|
36
|
+
|
|
30
37
|
static API = {
|
|
31
38
|
/**
|
|
32
39
|
* @method callback
|
|
@@ -356,7 +363,7 @@ class UnderpostBaremetal {
|
|
|
356
363
|
|
|
357
364
|
// Build phase (skip if upload-only mode)
|
|
358
365
|
if (options.packerMaasImageBuild) {
|
|
359
|
-
if (shellExec('packer version').code !== 0) {
|
|
366
|
+
if (shellExec('packer version', { silentOnError: true }).code !== 0) {
|
|
360
367
|
throw new Error('Packer is not installed. Please install Packer to proceed.');
|
|
361
368
|
}
|
|
362
369
|
|
|
@@ -424,7 +431,9 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
424
431
|
const uploadCmd = `${uploadScript} ${process.env.MAAS_ADMIN_USERNAME} "${workflow.maas.name}" "${workflow.maas.title}" "${workflow.maas.architecture}" "${workflow.maas.base_image}" "${workflow.maas.filetype}" "${tarballPath}"`;
|
|
425
432
|
|
|
426
433
|
logger.info(`Uploading to MAAS using: ${uploadScript}`);
|
|
427
|
-
|
|
434
|
+
// silentOnError: caller logs stdout/stderr structure on failure
|
|
435
|
+
// before throwing its own, more informative error.
|
|
436
|
+
const uploadResult = shellExec(uploadCmd, { silentOnError: true });
|
|
428
437
|
if (uploadResult.code !== 0) {
|
|
429
438
|
logger.error(`Upload failed with exit code: ${uploadResult.code}`);
|
|
430
439
|
if (uploadResult.stdout) {
|
|
@@ -496,10 +505,13 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
496
505
|
|
|
497
506
|
// Handle control server installation.
|
|
498
507
|
if (options.controlServerInstall === true) {
|
|
499
|
-
// Ensure
|
|
508
|
+
// Ensure the MAAS setup script is executable and then run it.
|
|
500
509
|
shellExec(`chmod +x ${underpostRoot}/scripts/maas-setup.sh`);
|
|
501
|
-
|
|
510
|
+
if (!fs.existsSync(`${process.env.HOME}/.ssh/id_rsa.pub`)) shellExec(`node bin ssh --generate`);
|
|
502
511
|
shellExec(`${underpostRoot}/scripts/maas-setup.sh`);
|
|
512
|
+
// Install GRUB modules into the NFS root filesystem to
|
|
513
|
+
// ensure the necessary files are present for bootloader installation later.
|
|
514
|
+
Underpost.baremetal.installGrubModules();
|
|
503
515
|
return;
|
|
504
516
|
}
|
|
505
517
|
|
|
@@ -599,7 +611,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
599
611
|
}
|
|
600
612
|
|
|
601
613
|
// Create a podman container to extract QEMU static binaries.
|
|
602
|
-
shellExec(`sudo podman create --name extract multiarch/qemu-user-static`);
|
|
614
|
+
shellExec(`sudo podman create --name extract docker.io/multiarch/qemu-user-static`);
|
|
603
615
|
shellExec(`podman ps -a`); // List all podman containers for verification.
|
|
604
616
|
|
|
605
617
|
// If cross-architecture, copy the QEMU static binary into the chroot.
|
|
@@ -915,9 +927,6 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
915
927
|
isoUrl: workflowsConfig[workflowId].isoUrl,
|
|
916
928
|
});
|
|
917
929
|
|
|
918
|
-
// Set up iptables rules for NAT and port forwarding to enable network connectivity for the baremetal machines.
|
|
919
|
-
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`, { silent: true });
|
|
920
|
-
|
|
921
930
|
// Start HTTP bootstrap server if commissioning or if ISO URL is used (for ISO-based workflows).
|
|
922
931
|
if (options.bootstrapHttpServerRun || options.commission) {
|
|
923
932
|
Underpost.baremetal.httpBootstrapServerRunnerFactory({
|
|
@@ -931,7 +940,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
931
940
|
});
|
|
932
941
|
}
|
|
933
942
|
|
|
934
|
-
// Rebuild NFS
|
|
943
|
+
// Rebuild NFS exports and the matching MAAS/firewalld host configuration.
|
|
935
944
|
if (
|
|
936
945
|
(options.nfsBuildServer === true || options.commission === true) &&
|
|
937
946
|
(workflowsConfig[workflowId].type === 'iso-nfs' ||
|
|
@@ -941,6 +950,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
941
950
|
Underpost.baremetal.rebuildNfsServer({
|
|
942
951
|
nfsHostPath,
|
|
943
952
|
nfsReset: options.nfsReset,
|
|
953
|
+
underpostRoot,
|
|
944
954
|
});
|
|
945
955
|
|
|
946
956
|
// Handle commissioning tasks
|
|
@@ -1397,7 +1407,9 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1397
1407
|
shellExec(`mkdir -p ${mountPoint}`);
|
|
1398
1408
|
|
|
1399
1409
|
// Ensure mount point is not already mounted
|
|
1400
|
-
shellExec(`sudo umount ${mountPoint}
|
|
1410
|
+
shellExec(`sudo umount ${mountPoint}`, {
|
|
1411
|
+
silentOnError: true, // Ignore errors if not mounted
|
|
1412
|
+
});
|
|
1401
1413
|
|
|
1402
1414
|
try {
|
|
1403
1415
|
// Mount the ISO
|
|
@@ -2340,39 +2352,36 @@ fi
|
|
|
2340
2352
|
// Determine OS family from osIdLike
|
|
2341
2353
|
const { isDebianBased, isRhelBased } = Underpost.baremetal.getFamilyBaseOs(options.osIdLike);
|
|
2342
2354
|
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
}`;
|
|
2371
|
-
|
|
2372
|
-
const
|
|
2373
|
-
nfsOptions ? `,${nfsOptions}` : ''
|
|
2374
|
-
}`;
|
|
2375
|
-
|
|
2355
|
+
const ifaceName = networkInterfaceName ? networkInterfaceName : 'eth0';
|
|
2356
|
+
const isStaticIp = ipConfig === 'none' || ipConfig === 'off';
|
|
2357
|
+
const ipParam = isStaticIp
|
|
2358
|
+
? `ip=${ipClient}:${ipFileServer}:${ipDhcpServer}:${netmask}:${hostname}:${ifaceName}:${ipConfig}:${dnsServer}`
|
|
2359
|
+
: `ip=::::${hostname}:${ifaceName}:${ipConfig}`;
|
|
2360
|
+
|
|
2361
|
+
let nfsMountOptions = [];
|
|
2362
|
+
if (type === 'chroot-debootstrap' || type === 'chroot-container') {
|
|
2363
|
+
nfsMountOptions = [
|
|
2364
|
+
'tcp',
|
|
2365
|
+
'nfsvers=3',
|
|
2366
|
+
'nolock',
|
|
2367
|
+
'port=2049',
|
|
2368
|
+
'hard',
|
|
2369
|
+
'intr',
|
|
2370
|
+
'rsize=32768',
|
|
2371
|
+
'wsize=32768',
|
|
2372
|
+
'acregmin=0',
|
|
2373
|
+
'acregmax=0',
|
|
2374
|
+
'acdirmin=0',
|
|
2375
|
+
'acdirmax=0',
|
|
2376
|
+
'noac',
|
|
2377
|
+
];
|
|
2378
|
+
} else if (type === 'iso-nfs' && isDebianBased) {
|
|
2379
|
+
nfsMountOptions = ['nolock', 'nfsvers=3', 'tcp', 'hard', 'port=2049', 'rsize=32768', 'wsize=32768'];
|
|
2380
|
+
}
|
|
2381
|
+
const nfsOptions = nfsMountOptions.join(',');
|
|
2382
|
+
const nfsServerPath = `${ipFileServer}:${process.env.NFS_EXPORT_PATH}/${hostname}`;
|
|
2383
|
+
const nfsRootParam = `nfsroot=${nfsServerPath}${nfsOptions ? `,${nfsOptions}` : ''}`;
|
|
2384
|
+
const casperNfsParams = [`nfsroot=${nfsServerPath}`, ...(nfsOptions ? [`nfsopts=${nfsOptions}`] : [])];
|
|
2376
2385
|
const permissionsParams = [
|
|
2377
2386
|
`rw`,
|
|
2378
2387
|
// `ro`
|
|
@@ -2445,8 +2454,12 @@ fi
|
|
|
2445
2454
|
let qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`];
|
|
2446
2455
|
cmd = [ipParam, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
|
|
2447
2456
|
} else {
|
|
2448
|
-
// 'iso-nfs' —
|
|
2449
|
-
|
|
2457
|
+
// 'iso-nfs' — kernel/initrd from ISO, root filesystem served via NFS.
|
|
2458
|
+
if (isDebianBased) {
|
|
2459
|
+
cmd = [ipParam, `boot=casper`, `netboot=nfs`, ...casperNfsParams, ...kernelParams];
|
|
2460
|
+
} else {
|
|
2461
|
+
cmd = [ipParam, `netboot=nfs`, nfsRootParam, ...kernelParams, ...performanceParams];
|
|
2462
|
+
}
|
|
2450
2463
|
}
|
|
2451
2464
|
|
|
2452
2465
|
// Add RHEL/Rocky/Fedora based images specific parameters
|
|
@@ -2456,7 +2469,7 @@ fi
|
|
|
2456
2469
|
}
|
|
2457
2470
|
// Add Debian/Ubuntu based images specific parameters
|
|
2458
2471
|
else if (isDebianBased) {
|
|
2459
|
-
cmd = cmd.concat([`initrd=initrd.img`, `init=/sbin/init`]);
|
|
2472
|
+
if (type !== 'iso-nfs') cmd = cmd.concat([`initrd=initrd.img`, `init=/sbin/init`]);
|
|
2460
2473
|
if (options.dev) cmd = cmd.concat([`debug`, `ignore_loglevel`]);
|
|
2461
2474
|
}
|
|
2462
2475
|
|
|
@@ -2702,7 +2715,7 @@ fi
|
|
|
2702
2715
|
shellExec(`sudo podman run --rm --privileged docker.io/multiarch/qemu-user-static:latest --reset -p yes`);
|
|
2703
2716
|
// Mount binfmt_misc filesystem.
|
|
2704
2717
|
shellExec(`sudo modprobe binfmt_misc`);
|
|
2705
|
-
shellExec(`sudo mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc
|
|
2718
|
+
shellExec(`sudo mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc`, { silentOnError: true });
|
|
2706
2719
|
},
|
|
2707
2720
|
|
|
2708
2721
|
/**
|
|
@@ -2759,6 +2772,19 @@ fi
|
|
|
2759
2772
|
await Underpost.baremetal.macMonitor({ nfsHostPath });
|
|
2760
2773
|
},
|
|
2761
2774
|
|
|
2775
|
+
/**
|
|
2776
|
+
* @method installGrubModules
|
|
2777
|
+
* @description Installs the necessary GRUB modules for both ARM64 and AMD64 architectures.
|
|
2778
|
+
* This ensures that the GRUB bootloader can properly load the kernel and initrd images
|
|
2779
|
+
* during the network boot process, regardless of the target architecture.
|
|
2780
|
+
* @memberof UnderpostBaremetal
|
|
2781
|
+
* @returns {void}
|
|
2782
|
+
*/
|
|
2783
|
+
installGrubModules() {
|
|
2784
|
+
if (!fs.existsSync('/usr/lib/grub/x86_64-efi')) shellExec(`sudo dnf install -y grub2-efi-x64-modules`);
|
|
2785
|
+
if (!fs.existsSync('/usr/lib/grub/arm64-efi')) shellExec(`sudo dnf install -y grub2-efi-aa64-modules`);
|
|
2786
|
+
},
|
|
2787
|
+
|
|
2762
2788
|
/**
|
|
2763
2789
|
* @method crossArchBinFactory
|
|
2764
2790
|
* @description Copies the appropriate QEMU static binary into the NFS root filesystem
|
|
@@ -2784,9 +2810,6 @@ fi
|
|
|
2784
2810
|
logger.warn(`Unsupported bootstrap architecture: ${bootstrapArch}`);
|
|
2785
2811
|
break;
|
|
2786
2812
|
}
|
|
2787
|
-
// Install GRUB EFI modules for both architectures to ensure compatibility.
|
|
2788
|
-
shellExec(`sudo dnf install -y grub2-efi-aa64-modules`);
|
|
2789
|
-
shellExec(`sudo dnf install -y grub2-efi-x64-modules`);
|
|
2790
2813
|
},
|
|
2791
2814
|
|
|
2792
2815
|
/**
|
|
@@ -2895,9 +2918,17 @@ EOF`);
|
|
|
2895
2918
|
for (const mountPath of mounts[mountCmd]) {
|
|
2896
2919
|
const hostMountPath = `${process.env.NFS_EXPORT_PATH}/${hostname}${mountPath}`;
|
|
2897
2920
|
// Check if the path is already mounted using `mountpoint` command.
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2921
|
+
// `mountpoint` exits 1 when the path is not a mountpoint — silentOnError
|
|
2922
|
+
// prevents ShellExecError so we can inspect stdout/stderr for the string.
|
|
2923
|
+
const mountpointOut = shellExec(`mountpoint ${hostMountPath}`, {
|
|
2924
|
+
silent: true,
|
|
2925
|
+
stdout: true,
|
|
2926
|
+
silentOnError: true,
|
|
2927
|
+
});
|
|
2928
|
+
const isPathMounted =
|
|
2929
|
+
typeof mountpointOut === 'string' && mountpointOut.length > 0
|
|
2930
|
+
? !mountpointOut.match('not a mountpoint') && !mountpointOut.match('No such file')
|
|
2931
|
+
: false;
|
|
2901
2932
|
|
|
2902
2933
|
if (isPathMounted) {
|
|
2903
2934
|
logger.warn('Nfs path already mounted', mountPath);
|
|
@@ -2957,54 +2988,48 @@ EOF`);
|
|
|
2957
2988
|
|
|
2958
2989
|
/**
|
|
2959
2990
|
* @method rebuildNfsServer
|
|
2960
|
-
* @description Configures and
|
|
2961
|
-
* This is crucial for allowing baremetal machines to boot via NFS.
|
|
2991
|
+
* @description Configures NFS exports and aligns host firewall/NFS daemon ports for MAAS workflows.
|
|
2962
2992
|
* @param {object} params - The parameters for the function.
|
|
2963
2993
|
* @param {string} params.nfsHostPath - The path to the NFS server export.
|
|
2964
2994
|
* @memberof UnderpostBaremetal
|
|
2965
2995
|
* @param {string} [params.subnet='192.168.1.0/24'] - The subnet allowed to access the NFS export.
|
|
2966
2996
|
* @param {boolean} [params.nfsReset=false] - Flag to completely reset the NFS server (restart service).
|
|
2997
|
+
* @param {string} [params.underpostRoot] - Repository root used to locate helper scripts.
|
|
2967
2998
|
* @returns {void}
|
|
2968
2999
|
*/
|
|
2969
|
-
rebuildNfsServer({ nfsHostPath, subnet, nfsReset }) {
|
|
3000
|
+
rebuildNfsServer({ nfsHostPath, subnet, nfsReset, underpostRoot }) {
|
|
2970
3001
|
if (!subnet) subnet = '192.168.1.0/24'; // Default subnet if not provided.
|
|
3002
|
+
if (!underpostRoot) {
|
|
3003
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
3004
|
+
underpostRoot = path.resolve(__dirname, '../..');
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
const maasNatFirewalldPath = path.resolve(underpostRoot, 'scripts/maas-nat-firewalld.sh');
|
|
3008
|
+
const nfsPorts = UnderpostBaremetal.NFS_V3_PORTS;
|
|
3009
|
+
const exportOptions = ['rw', 'sync', 'no_root_squash', 'no_subtree_check', 'insecure'].join(',');
|
|
3010
|
+
|
|
3011
|
+
if (!fs.existsSync(maasNatFirewalldPath)) {
|
|
3012
|
+
throw new Error(`MAAS firewalld helper not found: ${maasNatFirewalldPath}`);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
2971
3015
|
// Write the NFS exports configuration to /etc/exports.
|
|
2972
|
-
fs.
|
|
2973
|
-
|
|
2974
|
-
`${nfsHostPath} ${subnet}(${[
|
|
2975
|
-
'rw', // Read-write access.
|
|
2976
|
-
// 'all_squash', // Squash all client UIDs/GIDs to anonymous.
|
|
2977
|
-
'sync', // Synchronous writes.
|
|
2978
|
-
'no_root_squash', // Do not squash root user.
|
|
2979
|
-
'no_subtree_check', // Disable subtree checking.
|
|
2980
|
-
'insecure', // Allow connections from non-privileged ports.
|
|
2981
|
-
]})`,
|
|
2982
|
-
'utf8',
|
|
2983
|
-
);
|
|
3016
|
+
if (!fs.existsSync(nfsHostPath)) fs.mkdirSync(nfsHostPath, { recursive: true });
|
|
3017
|
+
fs.writeFileSync(`/etc/exports`, `${nfsHostPath} ${subnet}(${exportOptions})`, 'utf8');
|
|
2984
3018
|
|
|
2985
|
-
logger.info('
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
rdma=y
|
|
2999
|
-
rdma-port=20049
|
|
3000
|
-
|
|
3001
|
-
[lockd]
|
|
3002
|
-
port = 32766
|
|
3003
|
-
udp-port = 32766
|
|
3004
|
-
`,
|
|
3005
|
-
'utf8',
|
|
3019
|
+
logger.info('Configuring MAAS firewalld and fixed NFSv3 ports...');
|
|
3020
|
+
shellExec(
|
|
3021
|
+
[
|
|
3022
|
+
`MAAS_LAN_CIDR=${subnet}`,
|
|
3023
|
+
`NFS_MODE=v3`,
|
|
3024
|
+
`CONFIGURE_NFS_V3_PORTS=true`,
|
|
3025
|
+
`NFS_MOUNTD_PORT=${nfsPorts.mountd}`,
|
|
3026
|
+
`NFS_STATD_PORT=${nfsPorts.statd}`,
|
|
3027
|
+
`NFS_STATD_OUTGOING_PORT=${nfsPorts.statdOutgoing}`,
|
|
3028
|
+
`NFS_LOCKD_PORT=${nfsPorts.lockd}`,
|
|
3029
|
+
`NFS_LOCKD_UDP_PORT=${nfsPorts.lockd}`,
|
|
3030
|
+
`bash "${maasNatFirewalldPath}"`,
|
|
3031
|
+
].join(' '),
|
|
3006
3032
|
);
|
|
3007
|
-
logger.info('NFS configuration written.');
|
|
3008
3033
|
|
|
3009
3034
|
logger.info('Reloading NFS exports...');
|
|
3010
3035
|
shellExec(`sudo exportfs -rav`);
|
|
@@ -3013,12 +3038,18 @@ udp-port = 32766
|
|
|
3013
3038
|
logger.info('Displaying active NFS exports');
|
|
3014
3039
|
shellExec(`sudo exportfs -s`);
|
|
3015
3040
|
|
|
3016
|
-
// Restart the nfs-server service to apply all configuration changes,
|
|
3017
|
-
// including port settings from /etc/nfs.conf and export changes.
|
|
3018
3041
|
if (nfsReset) {
|
|
3019
|
-
logger.info('Restarting
|
|
3020
|
-
|
|
3021
|
-
|
|
3042
|
+
logger.info('Restarting NFS server service...');
|
|
3043
|
+
let restarted = false;
|
|
3044
|
+
for (const unit of ['nfs-server', 'nfs-kernel-server']) {
|
|
3045
|
+
const result = shellExec(`sudo systemctl restart ${unit}`, { silentOnError: true });
|
|
3046
|
+
if (result.code === 0) {
|
|
3047
|
+
restarted = true;
|
|
3048
|
+
logger.info(`NFS server restarted via ${unit}.`);
|
|
3049
|
+
break;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
if (!restarted) logger.warn('Unable to restart nfs-server or nfs-kernel-server after export reload.');
|
|
3022
3053
|
}
|
|
3023
3054
|
},
|
|
3024
3055
|
|
|
@@ -3041,10 +3072,10 @@ udp-port = 32766
|
|
|
3041
3072
|
// Check both /usr/local/bin (compiled) and system paths
|
|
3042
3073
|
let qemuAarch64Path = null;
|
|
3043
3074
|
|
|
3044
|
-
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64').code === 0) {
|
|
3075
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64', { silentOnError: true }).code === 0) {
|
|
3045
3076
|
qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
|
|
3046
|
-
} else if (shellExec('which qemu-system-aarch64').code === 0) {
|
|
3047
|
-
qemuAarch64Path = shellExec('which qemu-system-aarch64').
|
|
3077
|
+
} else if (shellExec('which qemu-system-aarch64', { silentOnError: true }).code === 0) {
|
|
3078
|
+
qemuAarch64Path = shellExec('which qemu-system-aarch64', { stdout: true }).trim();
|
|
3048
3079
|
}
|
|
3049
3080
|
|
|
3050
3081
|
if (!qemuAarch64Path) {
|
|
@@ -3070,10 +3101,10 @@ udp-port = 32766
|
|
|
3070
3101
|
// Check both /usr/local/bin (compiled) and system paths
|
|
3071
3102
|
let qemuX86Path = null;
|
|
3072
3103
|
|
|
3073
|
-
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64').code === 0) {
|
|
3104
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64', { silentOnError: true }).code === 0) {
|
|
3074
3105
|
qemuX86Path = '/usr/local/bin/qemu-system-x86_64';
|
|
3075
|
-
} else if (shellExec('which qemu-system-x86_64').code === 0) {
|
|
3076
|
-
qemuX86Path = shellExec('which qemu-system-x86_64').
|
|
3106
|
+
} else if (shellExec('which qemu-system-x86_64', { silentOnError: true }).code === 0) {
|
|
3107
|
+
qemuX86Path = shellExec('which qemu-system-x86_64', { stdout: true }).trim();
|
|
3077
3108
|
}
|
|
3078
3109
|
|
|
3079
3110
|
if (!qemuX86Path) {
|