underpost 2.95.8 → 2.96.1
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/README.md +2 -2
- package/baremetal/commission-workflows.json +44 -0
- package/baremetal/packer-workflows.json +24 -0
- package/cli.md +29 -31
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/packer/images/Rocky9Amd64/Makefile +62 -0
- package/packer/images/Rocky9Amd64/QUICKSTART.md +113 -0
- package/packer/images/Rocky9Amd64/README.md +122 -0
- package/packer/images/Rocky9Amd64/http/rocky9.ks.pkrtpl.hcl +114 -0
- package/packer/images/Rocky9Amd64/rocky9.pkr.hcl +164 -0
- package/packer/images/Rocky9Arm64/Makefile +69 -0
- package/packer/images/Rocky9Arm64/README.md +122 -0
- package/packer/images/Rocky9Arm64/http/rocky9.ks.pkrtpl.hcl +114 -0
- package/packer/images/Rocky9Arm64/rocky9.pkr.hcl +171 -0
- package/packer/scripts/fuse-nbd +64 -0
- package/packer/scripts/fuse-tar-root +63 -0
- package/scripts/maas-setup.sh +13 -2
- package/scripts/maas-upload-boot-resource.sh +183 -0
- package/scripts/packer-init-vars-file.sh +40 -0
- package/scripts/packer-setup.sh +289 -0
- package/src/cli/baremetal.js +342 -55
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/env.js +24 -3
- package/src/cli/index.js +19 -0
- package/src/cli/repository.js +164 -0
- package/src/index.js +2 -1
- package/manifests/mariadb/config.yaml +0 -10
- package/manifests/mariadb/secret.yaml +0 -8
- package/src/client/ssr/pages/404.js +0 -12
- package/src/client/ssr/pages/500.js +0 -12
- package/src/client/ssr/pages/maintenance.js +0 -14
- package/src/client/ssr/pages/offline.js +0 -21
package/src/cli/baremetal.js
CHANGED
|
@@ -4,15 +4,19 @@
|
|
|
4
4
|
* @namespace UnderpostBaremetal
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
7
8
|
import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
|
|
8
9
|
import { openTerminal, pbcopy, shellExec } from '../server/process.js';
|
|
9
10
|
import dotenv from 'dotenv';
|
|
10
11
|
import { loggerFactory } from '../server/logger.js';
|
|
11
12
|
import { getLocalIPv4Address } from '../server/dns.js';
|
|
12
13
|
import fs from 'fs-extra';
|
|
14
|
+
import path from 'path';
|
|
13
15
|
import Downloader from '../server/downloader.js';
|
|
14
16
|
import UnderpostCloudInit from './cloud-init.js';
|
|
17
|
+
import UnderpostRepository from './repository.js';
|
|
15
18
|
import { s4, timer } from '../client/components/core/CommonJs.js';
|
|
19
|
+
import { spawnSync } from 'child_process';
|
|
16
20
|
|
|
17
21
|
const logger = loggerFactory(import.meta);
|
|
18
22
|
|
|
@@ -25,6 +29,19 @@ const logger = loggerFactory(import.meta);
|
|
|
25
29
|
*/
|
|
26
30
|
class UnderpostBaremetal {
|
|
27
31
|
static API = {
|
|
32
|
+
/**
|
|
33
|
+
* @method installPacker
|
|
34
|
+
* @description Installs Packer CLI.
|
|
35
|
+
* @memberof UnderpostBaremetal
|
|
36
|
+
* @returns {Promise<void>}
|
|
37
|
+
*/
|
|
38
|
+
async installPacker(underpostRoot) {
|
|
39
|
+
const scriptPath = `${underpostRoot}/scripts/packer-setup.sh`;
|
|
40
|
+
logger.info(`Installing Packer using script: ${scriptPath}`);
|
|
41
|
+
shellExec(`sudo chmod +x ${scriptPath}`);
|
|
42
|
+
shellExec(`sudo ${scriptPath}`);
|
|
43
|
+
},
|
|
44
|
+
|
|
28
45
|
/**
|
|
29
46
|
* @method callback
|
|
30
47
|
* @description Initiates a baremetal provisioning workflow based on the provided options.
|
|
@@ -40,6 +57,11 @@ class UnderpostBaremetal {
|
|
|
40
57
|
* @param {boolean} [options.controlServerUninstall=false] - Flag to uninstall the control server.
|
|
41
58
|
* @param {boolean} [options.controlServerDbInstall=false] - Flag to install the control server's database.
|
|
42
59
|
* @param {boolean} [options.controlServerDbUninstall=false] - Flag to uninstall the control server's database.
|
|
60
|
+
* @param {boolean} [options.installPacker=false] - Flag to install Packer CLI.
|
|
61
|
+
* @param {string} [options.packerMaasImageTemplate] - Template path from canonical/packer-maas to extract (requires workflow-id).
|
|
62
|
+
* @param {string} [options.packerWorkflowId] - Workflow ID for Packer MAAS image operations (used with --packer-maas-image-build or --packer-maas-image-upload).
|
|
63
|
+
* @param {boolean} [options.packerMaasImageBuild=false] - Flag to build a Packer MAAS image for the workflow specified by packerWorkflowId.
|
|
64
|
+
* @param {boolean} [options.packerMaasImageUpload=false] - Flag to upload a Packer MAAS image artifact without rebuilding for the workflow specified by packerWorkflowId.
|
|
43
65
|
* @param {boolean} [options.cloudInitUpdate=false] - Flag to update cloud-init configuration on the baremetal machine.
|
|
44
66
|
* @param {boolean} [options.commission=false] - Flag to commission the baremetal machine.
|
|
45
67
|
* @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
|
|
@@ -60,6 +82,12 @@ class UnderpostBaremetal {
|
|
|
60
82
|
controlServerUninstall: false,
|
|
61
83
|
controlServerDbInstall: false,
|
|
62
84
|
controlServerDbUninstall: false,
|
|
85
|
+
installPacker: false,
|
|
86
|
+
packerMaasImageTemplate: false,
|
|
87
|
+
packerWorkflowId: '',
|
|
88
|
+
packerMaasImageBuild: false,
|
|
89
|
+
packerMaasImageUpload: false,
|
|
90
|
+
packerMaasImageCached: false,
|
|
63
91
|
cloudInitUpdate: false,
|
|
64
92
|
commission: false,
|
|
65
93
|
nfsBuild: false,
|
|
@@ -108,6 +136,188 @@ class UnderpostBaremetal {
|
|
|
108
136
|
// Log the initiation of the baremetal callback with relevant metadata.
|
|
109
137
|
logger.info('Baremetal callback', callbackMetaData);
|
|
110
138
|
|
|
139
|
+
if (options.installPacker) {
|
|
140
|
+
await UnderpostBaremetal.API.installPacker(underpostRoot);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.packerMaasImageTemplate) {
|
|
145
|
+
workflowId = options.packerWorkflowId;
|
|
146
|
+
if (!workflowId) {
|
|
147
|
+
throw new Error('--packer-workflow-id is required when using --packer-maas-image-template');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const templatePath = options.packerMaasImageTemplate;
|
|
151
|
+
const targetDir = `${underpostRoot}/packer/images/${workflowId}`;
|
|
152
|
+
|
|
153
|
+
logger.info(`Creating new Packer MAAS image template for workflow: ${workflowId}`);
|
|
154
|
+
logger.info(`Template path: ${templatePath}`);
|
|
155
|
+
logger.info(`Target directory: ${targetDir}`);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Use UnderpostRepository to copy files from GitHub
|
|
159
|
+
const result = await UnderpostRepository.API.copyGitUrlDirectoryRecursive({
|
|
160
|
+
gitUrl: 'https://github.com/canonical/packer-maas',
|
|
161
|
+
directoryPath: templatePath,
|
|
162
|
+
targetPath: targetDir,
|
|
163
|
+
branch: 'main',
|
|
164
|
+
overwrite: false,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
logger.info(`\nSuccessfully copied ${result.filesCount} files`);
|
|
168
|
+
|
|
169
|
+
// Create empty workflow configuration entry
|
|
170
|
+
const workflowConfig = {
|
|
171
|
+
dir: `packer/images/${workflowId}`,
|
|
172
|
+
maas: {
|
|
173
|
+
name: `custom/${workflowId.toLowerCase()}`,
|
|
174
|
+
title: `${workflowId} Custom`,
|
|
175
|
+
architecture: 'amd64/generic',
|
|
176
|
+
base_image: 'ubuntu/22.04',
|
|
177
|
+
filetype: 'tgz',
|
|
178
|
+
content: `${workflowId.toLowerCase()}.tar.gz`,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const workflows = UnderpostBaremetal.API.loadPackerMaasImageBuildWorkflows();
|
|
183
|
+
workflows[workflowId] = workflowConfig;
|
|
184
|
+
UnderpostBaremetal.API.writePackerMaasImageBuildWorkflows(workflows);
|
|
185
|
+
|
|
186
|
+
logger.info('\nTemplate extracted successfully!');
|
|
187
|
+
logger.info(`\nAdded configuration for ${workflowId} to engine/baremetal/packer-workflows.json`);
|
|
188
|
+
logger.info('\nNext steps:');
|
|
189
|
+
logger.info(`1. Review and customize the Packer template files in: ${targetDir}`);
|
|
190
|
+
logger.info(`2. Review the workflow configuration in engine/baremetal/packer-workflows.json`);
|
|
191
|
+
logger.info(
|
|
192
|
+
`3. Build the image with: underpost baremetal --packer-workflow-id ${workflowId} --packer-maas-image-build`,
|
|
193
|
+
);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throw new Error(`Failed to extract template: ${error.message}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (options.packerMaasImageBuild || options.packerMaasImageUpload) {
|
|
202
|
+
// Use the workflow ID from --packer-workflow-id option
|
|
203
|
+
if (!options.packerWorkflowId) {
|
|
204
|
+
throw new Error('Workflow ID is required. Please specify using --packer-workflow-id <workflow-id>');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
workflowId = options.packerWorkflowId;
|
|
208
|
+
|
|
209
|
+
const workflow = UnderpostBaremetal.API.loadPackerMaasImageBuildWorkflows()[workflowId];
|
|
210
|
+
if (!workflow) {
|
|
211
|
+
throw new Error(`Packer MAAS image build workflow not found: ${workflowId}`);
|
|
212
|
+
}
|
|
213
|
+
const packerDir = `${underpostRoot}/${workflow.dir}`;
|
|
214
|
+
const tarballPath = `${packerDir}/${workflow.maas.content}`;
|
|
215
|
+
|
|
216
|
+
// Build phase (skip if upload-only mode)
|
|
217
|
+
if (options.packerMaasImageBuild) {
|
|
218
|
+
if (shellExec('packer version', { silent: true }).code !== 0) {
|
|
219
|
+
throw new Error('Packer is not installed. Please install Packer to proceed.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check for QEMU support if building for a different architecture (validator bots case)
|
|
223
|
+
UnderpostBaremetal.API.checkQemuCrossArchSupport(workflow);
|
|
224
|
+
|
|
225
|
+
logger.info(`Building Packer image for ${workflowId} in ${packerDir}...`);
|
|
226
|
+
|
|
227
|
+
// Only remove artifacts if not using cached mode
|
|
228
|
+
if (!options.packerMaasImageCached) {
|
|
229
|
+
const artifacts = [
|
|
230
|
+
'output-rocky9',
|
|
231
|
+
'packer_cache',
|
|
232
|
+
'x86_64_VARS.fd',
|
|
233
|
+
'aarch64_VARS.fd',
|
|
234
|
+
workflow.maas.content,
|
|
235
|
+
];
|
|
236
|
+
shellExec(`cd packer/images/${workflowId}
|
|
237
|
+
rm -rf ${artifacts.join(' ')}`);
|
|
238
|
+
logger.info('Removed previous build artifacts');
|
|
239
|
+
} else {
|
|
240
|
+
logger.info('Cached mode: Keeping existing artifacts for incremental build');
|
|
241
|
+
}
|
|
242
|
+
shellExec(`chmod +x ${underpostRoot}/scripts/packer-init-vars-file.sh`);
|
|
243
|
+
shellExec(`${underpostRoot}/scripts/packer-init-vars-file.sh`);
|
|
244
|
+
|
|
245
|
+
const init = spawnSync('packer', ['init', '.'], { stdio: 'inherit', cwd: packerDir });
|
|
246
|
+
if (init.status !== 0) {
|
|
247
|
+
throw new Error('Packer init failed');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const isArm = process.arch === 'arm64';
|
|
251
|
+
// Add /usr/local/bin to PATH so Packer can find compiled QEMU binaries
|
|
252
|
+
const packerEnv = {
|
|
253
|
+
...process.env,
|
|
254
|
+
PACKER_LOG: '1',
|
|
255
|
+
PATH: `/usr/local/bin:${process.env.PATH || '/usr/bin:/bin'}`,
|
|
256
|
+
};
|
|
257
|
+
const build = spawnSync('packer', ['build', '-var', `host_is_arm=${isArm}`, '.'], {
|
|
258
|
+
stdio: 'inherit',
|
|
259
|
+
cwd: packerDir,
|
|
260
|
+
env: packerEnv,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (build.status !== 0) {
|
|
264
|
+
throw new Error('Packer build failed');
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
// Upload-only mode: verify tarball exists
|
|
268
|
+
logger.info(`Upload-only mode: checking for existing build artifact...`);
|
|
269
|
+
if (!fs.existsSync(tarballPath)) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Build artifact not found: ${tarballPath}\n` +
|
|
272
|
+
`Please build first with: --packer-workflow-id ${workflowId} --packer-maas-image-build`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const stats = fs.statSync(tarballPath);
|
|
276
|
+
logger.info(`Found existing artifact: ${tarballPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
logger.info(`Uploading image to MAAS...`);
|
|
280
|
+
|
|
281
|
+
// Detect MAAS profile from 'maas list' output
|
|
282
|
+
let maasProfile = process.env.MAAS_ADMIN_USERNAME;
|
|
283
|
+
if (!maasProfile) {
|
|
284
|
+
const profileList = shellExec('maas list', { silent: true, stdout: true });
|
|
285
|
+
if (profileList) {
|
|
286
|
+
const firstLine = profileList.trim().split('\n')[0];
|
|
287
|
+
const match = firstLine.match(/^(\S+)\s+http/);
|
|
288
|
+
if (match) {
|
|
289
|
+
maasProfile = match[1];
|
|
290
|
+
logger.info(`Detected MAAS profile: ${maasProfile}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!maasProfile) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
'MAAS profile not found. Please run "maas login" first or set MAAS_ADMIN_USERNAME environment variable.',
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Use the upload script to avoid MAAS CLI bugs
|
|
302
|
+
const uploadScript = `${underpostRoot}/scripts/maas-upload-boot-resource.sh`;
|
|
303
|
+
const uploadCmd = `${uploadScript} ${maasProfile} "${workflow.maas.name}" "${workflow.maas.title}" "${workflow.maas.architecture}" "${workflow.maas.base_image}" "${workflow.maas.filetype}" "${tarballPath}"`;
|
|
304
|
+
|
|
305
|
+
logger.info(`Uploading to MAAS using: ${uploadScript}`);
|
|
306
|
+
const uploadResult = shellExec(uploadCmd);
|
|
307
|
+
if (uploadResult.code !== 0) {
|
|
308
|
+
logger.error(`Upload failed with exit code: ${uploadResult.code}`);
|
|
309
|
+
if (uploadResult.stdout) {
|
|
310
|
+
logger.error(`Upload output:\n${uploadResult.stdout}`);
|
|
311
|
+
}
|
|
312
|
+
if (uploadResult.stderr) {
|
|
313
|
+
logger.error(`Upload error output:\n${uploadResult.stderr}`);
|
|
314
|
+
}
|
|
315
|
+
throw new Error('MAAS upload failed - see output above for details');
|
|
316
|
+
}
|
|
317
|
+
logger.info(`Successfully uploaded ${workflow.maas.name} to MAAS!`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
111
321
|
// Handle various log display options.
|
|
112
322
|
if (options.logs === 'dhcp') {
|
|
113
323
|
shellExec(`journalctl -f -t dhcpd -u snap.maas.pebble.service`);
|
|
@@ -131,7 +341,11 @@ class UnderpostBaremetal {
|
|
|
131
341
|
|
|
132
342
|
// Handle NFS shell access option.
|
|
133
343
|
if (options.nfsSh === true) {
|
|
134
|
-
const
|
|
344
|
+
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
345
|
+
if (!workflowsConfig[workflowId]) {
|
|
346
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
347
|
+
}
|
|
348
|
+
const { debootstrap } = workflowsConfig[workflowId];
|
|
135
349
|
// Copy the chroot command to the clipboard for easy execution.
|
|
136
350
|
if (debootstrap.image.architecture !== callbackMetaData.runnerHost.architecture)
|
|
137
351
|
switch (debootstrap.image.architecture) {
|
|
@@ -196,9 +410,14 @@ class UnderpostBaremetal {
|
|
|
196
410
|
return;
|
|
197
411
|
}
|
|
198
412
|
|
|
413
|
+
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
414
|
+
if (!workflowsConfig[workflowId]) {
|
|
415
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
199
418
|
// Set debootstrap architecture.
|
|
200
419
|
{
|
|
201
|
-
const { architecture } =
|
|
420
|
+
const { architecture } = workflowsConfig[workflowId].debootstrap.image;
|
|
202
421
|
debootstrapArch = architecture;
|
|
203
422
|
}
|
|
204
423
|
|
|
@@ -228,7 +447,7 @@ class UnderpostBaremetal {
|
|
|
228
447
|
|
|
229
448
|
// Perform the first stage of debootstrap.
|
|
230
449
|
{
|
|
231
|
-
const { architecture, name } =
|
|
450
|
+
const { architecture, name } = workflowsConfig[workflowId].debootstrap.image;
|
|
232
451
|
shellExec(
|
|
233
452
|
[
|
|
234
453
|
`sudo debootstrap`,
|
|
@@ -273,7 +492,7 @@ class UnderpostBaremetal {
|
|
|
273
492
|
|
|
274
493
|
// Apply system provisioning steps (base, user, timezone, keyboard).
|
|
275
494
|
{
|
|
276
|
-
const { systemProvisioning, kernelLibVersion, chronyc } =
|
|
495
|
+
const { systemProvisioning, kernelLibVersion, chronyc } = workflowsConfig[workflowId];
|
|
277
496
|
const { timezone, chronyConfPath } = chronyc;
|
|
278
497
|
|
|
279
498
|
UnderpostBaremetal.API.crossArchRunner({
|
|
@@ -327,8 +546,7 @@ class UnderpostBaremetal {
|
|
|
327
546
|
|
|
328
547
|
// Handle commissioning tasks (placeholder for future implementation).
|
|
329
548
|
if (options.commission === true) {
|
|
330
|
-
const { firmwares, networkInterfaceName, maas, netmask, menuentryStr } =
|
|
331
|
-
UnderpostBaremetal.API.workflowsConfig[workflowId];
|
|
549
|
+
const { firmwares, networkInterfaceName, maas, netmask, menuentryStr } = workflowsConfig[workflowId];
|
|
332
550
|
const resource = resources.find(
|
|
333
551
|
(o) => o.architecture === maas.image.architecture && o.name === maas.image.name,
|
|
334
552
|
);
|
|
@@ -490,7 +708,7 @@ menuentry '${menuentryStr}' {
|
|
|
490
708
|
|
|
491
709
|
// Final commissioning steps.
|
|
492
710
|
if (options.commission === true || options.cloudInitUpdate === true) {
|
|
493
|
-
const { debootstrap, networkInterfaceName, chronyc, maas } =
|
|
711
|
+
const { debootstrap, networkInterfaceName, chronyc, maas } = workflowsConfig[workflowId];
|
|
494
712
|
const { timezone, chronyConfPath } = chronyc;
|
|
495
713
|
|
|
496
714
|
// Build cloud-init tools.
|
|
@@ -742,7 +960,7 @@ menuentry '${menuentryStr}' {
|
|
|
742
960
|
// Install necessary packages for debootstrap and QEMU.
|
|
743
961
|
shellExec(`sudo dnf install -y iptables-legacy`);
|
|
744
962
|
shellExec(`sudo dnf install -y debootstrap`);
|
|
745
|
-
shellExec(`sudo dnf install kernel-modules-extra-$(uname -r)`);
|
|
963
|
+
shellExec(`sudo dnf install -y kernel-modules-extra-$(uname -r)`);
|
|
746
964
|
// Reset QEMU user-static binfmt for proper cross-architecture execution.
|
|
747
965
|
shellExec(`sudo podman run --rm --privileged docker.io/multiarch/qemu-user-static:latest --reset -p yes`);
|
|
748
966
|
// Mount binfmt_misc filesystem.
|
|
@@ -914,9 +1132,13 @@ EOF`);
|
|
|
914
1132
|
*/
|
|
915
1133
|
nfsMountCallback({ hostname, workflowId, mount, unmount }) {
|
|
916
1134
|
let isMounted = false;
|
|
1135
|
+
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
1136
|
+
if (!workflowsConfig[workflowId]) {
|
|
1137
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
1138
|
+
}
|
|
917
1139
|
// Iterate through defined NFS mounts in the workflow configuration.
|
|
918
|
-
for (const mountCmd of Object.keys(
|
|
919
|
-
for (const mountPath of
|
|
1140
|
+
for (const mountCmd of Object.keys(workflowsConfig[workflowId].nfs.mounts)) {
|
|
1141
|
+
for (const mountPath of workflowsConfig[workflowId].nfs.mounts[mountCmd]) {
|
|
920
1142
|
const hostMountPath = `${process.env.NFS_EXPORT_PATH}/${hostname}${mountPath}`;
|
|
921
1143
|
// Check if the path is already mounted using `mountpoint` command.
|
|
922
1144
|
const isPathMounted = !shellExec(`mountpoint ${hostMountPath}`, { silent: true, stdout: true }).match(
|
|
@@ -1191,6 +1413,80 @@ udp-port = 32766
|
|
|
1191
1413
|
logger.info('NFS server restarted.');
|
|
1192
1414
|
},
|
|
1193
1415
|
|
|
1416
|
+
/**
|
|
1417
|
+
* @method checkQemuCrossArchSupport
|
|
1418
|
+
* @description Checks for QEMU support when building for a different architecture.
|
|
1419
|
+
* This is essential for validator bots that need to build images for architectures
|
|
1420
|
+
* different from the host system (e.g., building arm64 on x86_64 or vice versa).
|
|
1421
|
+
* @param {object} workflow - The workflow configuration object.
|
|
1422
|
+
* @param {object} workflow.maas - The MAAS configuration.
|
|
1423
|
+
* @param {string} workflow.maas.architecture - Target architecture (e.g., 'arm64/generic', 'amd64/generic').
|
|
1424
|
+
* @memberof UnderpostBaremetal
|
|
1425
|
+
* @throws {Error} If QEMU is not installed or doesn't support required machine types.
|
|
1426
|
+
* @returns {void}
|
|
1427
|
+
*/
|
|
1428
|
+
checkQemuCrossArchSupport(workflow) {
|
|
1429
|
+
// Check for QEMU support if building for a different architecture (validator bots case)
|
|
1430
|
+
if (workflow.maas.architecture.startsWith('arm64') && process.arch !== 'arm64') {
|
|
1431
|
+
// Building arm64/aarch64 on x86_64 host
|
|
1432
|
+
// Check both /usr/local/bin (compiled) and system paths
|
|
1433
|
+
let qemuAarch64Path = null;
|
|
1434
|
+
|
|
1435
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64', { silent: true }).code === 0) {
|
|
1436
|
+
qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
|
|
1437
|
+
} else if (shellExec('which qemu-system-aarch64', { silent: true }).code === 0) {
|
|
1438
|
+
qemuAarch64Path = shellExec('which qemu-system-aarch64', { silent: true }).stdout.trim();
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (!qemuAarch64Path) {
|
|
1442
|
+
throw new Error(
|
|
1443
|
+
'qemu-system-aarch64 is not installed. Please install it to build ARM64 images on x86_64 hosts.\n' +
|
|
1444
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
logger.info(`Found qemu-system-aarch64 at: ${qemuAarch64Path}`);
|
|
1449
|
+
|
|
1450
|
+
// Verify that the installed qemu supports the 'virt' machine type (required for arm64)
|
|
1451
|
+
const machineHelp = shellExec(`${qemuAarch64Path} -machine help`, { silent: true }).stdout;
|
|
1452
|
+
if (!machineHelp.includes('virt')) {
|
|
1453
|
+
throw new Error(
|
|
1454
|
+
'The installed qemu-system-aarch64 does not support the "virt" machine type.\n' +
|
|
1455
|
+
'This usually happens if qemu-system-aarch64 is a symlink to qemu-kvm on x86_64.\n' +
|
|
1456
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
} else if (workflow.maas.architecture.startsWith('amd64') && process.arch !== 'x64') {
|
|
1460
|
+
// Building amd64/x86_64 on aarch64 host
|
|
1461
|
+
// Check both /usr/local/bin (compiled) and system paths
|
|
1462
|
+
let qemuX86Path = null;
|
|
1463
|
+
|
|
1464
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64', { silent: true }).code === 0) {
|
|
1465
|
+
qemuX86Path = '/usr/local/bin/qemu-system-x86_64';
|
|
1466
|
+
} else if (shellExec('which qemu-system-x86_64', { silent: true }).code === 0) {
|
|
1467
|
+
qemuX86Path = shellExec('which qemu-system-x86_64', { silent: true }).stdout.trim();
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (!qemuX86Path) {
|
|
1471
|
+
throw new Error(
|
|
1472
|
+
'qemu-system-x86_64 is not installed. Please install it to build x86_64 images on aarch64 hosts.\n' +
|
|
1473
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
logger.info(`Found qemu-system-x86_64 at: ${qemuX86Path}`);
|
|
1478
|
+
|
|
1479
|
+
// Verify that the installed qemu supports the 'pc' or 'q35' machine type (required for x86_64)
|
|
1480
|
+
const machineHelp = shellExec(`${qemuX86Path} -machine help`, { silent: true }).stdout;
|
|
1481
|
+
if (!machineHelp.includes('pc') && !machineHelp.includes('q35')) {
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
'The installed qemu-system-x86_64 does not support the "pc" or "q35" machine type.\n' +
|
|
1484
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
|
|
1194
1490
|
/**
|
|
1195
1491
|
* @method bootConfFactory
|
|
1196
1492
|
* @description Generates the boot configuration file for specific workflows,
|
|
@@ -1262,55 +1558,46 @@ GATEWAY=${gateway}`;
|
|
|
1262
1558
|
},
|
|
1263
1559
|
|
|
1264
1560
|
/**
|
|
1265
|
-
* @
|
|
1266
|
-
* @
|
|
1561
|
+
* @method loadWorkflowsConfig
|
|
1562
|
+
* @namespace UnderpostBaremetal.API
|
|
1563
|
+
* @description Loads the commission workflows configuration from commission-workflows.json.
|
|
1267
1564
|
* Each workflow defines specific parameters like system provisioning type,
|
|
1268
1565
|
* kernel version, Chrony settings, debootstrap image details, and NFS mounts. *
|
|
1269
1566
|
* @memberof UnderpostBaremetal
|
|
1270
1567
|
*/
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
},
|
|
1305
|
-
},
|
|
1306
|
-
nfs: {
|
|
1307
|
-
mounts: {
|
|
1308
|
-
// Define NFS mount points and their types (bind, rbind).
|
|
1309
|
-
bind: ['/proc', '/sys', '/run'], // Standard bind mounts.
|
|
1310
|
-
rbind: ['/dev'], // Recursive bind mount for /dev.
|
|
1311
|
-
},
|
|
1312
|
-
},
|
|
1313
|
-
},
|
|
1568
|
+
loadWorkflowsConfig() {
|
|
1569
|
+
if (this._workflowsConfig) return this._workflowsConfig;
|
|
1570
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1571
|
+
const configPath = path.resolve(__dirname, '../../baremetal/commission-workflows.json');
|
|
1572
|
+
this._workflowsConfig = fs.readJsonSync(configPath);
|
|
1573
|
+
return this._workflowsConfig;
|
|
1574
|
+
},
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* @property {object} packerMaasImageBuildWorkflows
|
|
1578
|
+
* @description Configuration for PACKe mass image workflows.
|
|
1579
|
+
* @memberof UnderpostBaremetal
|
|
1580
|
+
*/
|
|
1581
|
+
loadPackerMaasImageBuildWorkflows() {
|
|
1582
|
+
if (this._packerMaasImageBuildWorkflows) return this._packerMaasImageBuildWorkflows;
|
|
1583
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1584
|
+
const configPath = path.resolve(__dirname, '../../baremetal/packer-workflows.json');
|
|
1585
|
+
this._packerMaasImageBuildWorkflows = fs.readJsonSync(configPath);
|
|
1586
|
+
return this._packerMaasImageBuildWorkflows;
|
|
1587
|
+
},
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Write Packer MAAS image build workflows configuration to file
|
|
1591
|
+
* @param {object} workflows - The workflows configuration object
|
|
1592
|
+
* @description Writes the Packer MAAS image build workflows to packer-workflows.json
|
|
1593
|
+
* @memberof UnderpostBaremetal
|
|
1594
|
+
*/
|
|
1595
|
+
writePackerMaasImageBuildWorkflows(workflows) {
|
|
1596
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1597
|
+
const configPath = path.resolve(__dirname, '../../baremetal/packer-workflows.json');
|
|
1598
|
+
fs.writeJsonSync(configPath, workflows, { spaces: 2 });
|
|
1599
|
+
this._packerMaasImageBuildWorkflows = workflows;
|
|
1600
|
+
return configPath;
|
|
1314
1601
|
},
|
|
1315
1602
|
};
|
|
1316
1603
|
}
|
package/src/cli/cloud-init.js
CHANGED
|
@@ -43,7 +43,7 @@ class UnderpostCloudInit {
|
|
|
43
43
|
buildTools({ workflowId, nfsHostPath, hostname, callbackMetaData, dev }) {
|
|
44
44
|
// Destructure workflow configuration for easier access.
|
|
45
45
|
const { systemProvisioning, chronyc, networkInterfaceName, debootstrap } =
|
|
46
|
-
UnderpostBaremetal.API.
|
|
46
|
+
UnderpostBaremetal.API.loadWorkflowsConfig()[workflowId];
|
|
47
47
|
const { timezone, chronyConfPath } = chronyc;
|
|
48
48
|
// Define the specific directory for underpost tools within the NFS host path.
|
|
49
49
|
const nfsHostToolsPath = `${nfsHostPath}/underpost`;
|
package/src/cli/env.js
CHANGED
|
@@ -74,17 +74,38 @@ class UnderpostRootEnv {
|
|
|
74
74
|
/**
|
|
75
75
|
* @method list
|
|
76
76
|
* @description Lists all environment variables in the underpost root environment.
|
|
77
|
+
* @param {string} key - Not used for list operation.
|
|
78
|
+
* @param {string} value - Not used for list operation.
|
|
79
|
+
* @param {object} options - Options for listing environment variables.
|
|
80
|
+
* @param {string} [options.filter] - Filter keyword to match against keys or values.
|
|
77
81
|
* @memberof UnderpostEnv
|
|
78
82
|
*/
|
|
79
|
-
list() {
|
|
83
|
+
list(key, value, options = {}) {
|
|
80
84
|
const exeRootPath = `${getNpmRootPath()}/underpost`;
|
|
81
85
|
const envPath = `${exeRootPath}/.env`;
|
|
82
86
|
if (!fs.existsSync(envPath)) {
|
|
83
87
|
logger.warn(`Empty environment variables`);
|
|
84
88
|
return {};
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
let env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
91
|
+
|
|
92
|
+
// Apply filter if provided
|
|
93
|
+
if (options.filter) {
|
|
94
|
+
const filterKeyword = options.filter.toLowerCase();
|
|
95
|
+
const filtered = {};
|
|
96
|
+
for (const [envKey, envValue] of Object.entries(env)) {
|
|
97
|
+
const keyMatch = envKey.toLowerCase().includes(filterKeyword);
|
|
98
|
+
const valueMatch = String(envValue).toLowerCase().includes(filterKeyword);
|
|
99
|
+
if (keyMatch || valueMatch) {
|
|
100
|
+
filtered[envKey] = envValue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
env = filtered;
|
|
104
|
+
logger.info(`underpost root (filtered by: ${options.filter})`, env);
|
|
105
|
+
} else {
|
|
106
|
+
logger.info('underpost root', env);
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
return env;
|
|
89
110
|
},
|
|
90
111
|
/**
|
package/src/cli/index.js
CHANGED
|
@@ -181,6 +181,7 @@ program
|
|
|
181
181
|
.argument('[key]', 'Optional: The specific configuration key to manage.')
|
|
182
182
|
.argument('[value]', 'Optional: The value to set for the configuration key.')
|
|
183
183
|
.option('--plain', 'Prints the configuration value in plain text.')
|
|
184
|
+
.option('--filter <keyword>', 'Filters the list by matching key or value (only for list operation).')
|
|
184
185
|
.description(`Manages Underpost configurations using various operators.`)
|
|
185
186
|
.action((...args) => Underpost.env[args[0]](args[1], args[2], args[3]));
|
|
186
187
|
|
|
@@ -610,6 +611,24 @@ program
|
|
|
610
611
|
.option('--control-server-uninstall', 'Uninstalls the baremetal control server.')
|
|
611
612
|
.option('--control-server-db-install', 'Installs up the database for the baremetal control server.')
|
|
612
613
|
.option('--control-server-db-uninstall', 'Uninstalls the database for the baremetal control server.')
|
|
614
|
+
.option('--install-packer', 'Installs Packer CLI.')
|
|
615
|
+
.option(
|
|
616
|
+
'--packer-maas-image-template <template-path>',
|
|
617
|
+
'Creates a new image folder from canonical/packer-maas template path (requires workflow-id).',
|
|
618
|
+
)
|
|
619
|
+
.option('--packer-workflow-id <workflow-id>', 'Specifies the workflow ID for Packer MAAS image operations.')
|
|
620
|
+
.option(
|
|
621
|
+
'--packer-maas-image-build',
|
|
622
|
+
'Builds a MAAS image using Packer for the workflow specified by --packer-workflow-id.',
|
|
623
|
+
)
|
|
624
|
+
.option(
|
|
625
|
+
'--packer-maas-image-upload',
|
|
626
|
+
'Uploads an existing MAAS image artifact without rebuilding for the workflow specified by --packer-workflow-id.',
|
|
627
|
+
)
|
|
628
|
+
.option(
|
|
629
|
+
'--packer-maas-image-cached',
|
|
630
|
+
'Continue last build without removing artifacts (used with --packer-maas-image-build).',
|
|
631
|
+
)
|
|
613
632
|
.option('--commission', 'Init workflow for commissioning a physical machine.')
|
|
614
633
|
.option('--nfs-build', 'Builds an NFS root filesystem for a workflow id config architecture using QEMU emulation.')
|
|
615
634
|
.option('--nfs-mount', 'Mounts the NFS root filesystem for a workflow id config architecture.')
|