@underpostnet/underpost 2.99.5 → 2.99.7
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/ghpkg.ci.yml +10 -25
- package/.github/workflows/npmpkg.ci.yml +13 -2
- package/CHANGELOG.md +496 -0
- package/README.md +4 -4
- package/baremetal/commission-workflows.json +43 -6
- package/bin/deploy.js +13 -0
- package/cli.md +84 -42
- package/examples/static-page/README.md +80 -13
- package/jsdoc.json +26 -5
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +47 -0
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +47 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +2 -4
- package/scripts/maas-setup.sh +13 -9
- package/scripts/rocky-kickstart.sh +294 -0
- package/src/cli/baremetal.js +237 -555
- package/src/cli/cloud-init.js +27 -45
- package/src/cli/index.js +52 -6
- package/src/cli/kickstart.js +149 -0
- package/src/cli/repository.js +166 -13
- package/src/cli/run.js +26 -19
- package/src/cli/ssh.js +1 -1
- package/src/cli/static.js +27 -1
- package/src/cli/system.js +332 -0
- package/src/client/components/core/Docs.js +22 -3
- package/src/db/DataBaseProvider.js +3 -3
- package/src/db/mariadb/MariaDB.js +3 -3
- package/src/db/mongo/MongooseDB.js +3 -3
- package/src/index.js +28 -5
- package/src/mailer/EmailRender.js +3 -3
- package/src/mailer/MailerProvider.js +4 -4
- package/src/server/backup.js +23 -5
- package/src/server/client-build-docs.js +29 -3
- package/src/server/conf.js +6 -27
- package/src/server/cron.js +354 -135
- package/src/server/dns.js +2 -0
package/src/cli/cloud-init.js
CHANGED
|
@@ -64,7 +64,7 @@ class UnderpostCloudInit {
|
|
|
64
64
|
fs.writeFileSync(
|
|
65
65
|
`${nfsHostToolsPath}/date.sh`,
|
|
66
66
|
Underpost.baremetal.stepsRender(
|
|
67
|
-
Underpost.
|
|
67
|
+
Underpost.system.factory[systemProvisioning].timezone({
|
|
68
68
|
timezone,
|
|
69
69
|
chronyConfPath,
|
|
70
70
|
}),
|
|
@@ -78,7 +78,7 @@ class UnderpostCloudInit {
|
|
|
78
78
|
fs.writeFileSync(
|
|
79
79
|
`${nfsHostToolsPath}/keyboard.sh`,
|
|
80
80
|
Underpost.baremetal.stepsRender(
|
|
81
|
-
Underpost.
|
|
81
|
+
Underpost.system.factory[systemProvisioning].keyboard(keyboard.layout),
|
|
82
82
|
false,
|
|
83
83
|
),
|
|
84
84
|
'utf8',
|
|
@@ -122,8 +122,6 @@ ${Underpost.baremetal.stepsRender(
|
|
|
122
122
|
`cloud-init modules --mode=config`,
|
|
123
123
|
`sleep 3`,
|
|
124
124
|
`cloud-init modules --mode=final`,
|
|
125
|
-
`sleep 3`,
|
|
126
|
-
`/underpost/enlistment.sh`,
|
|
127
125
|
],
|
|
128
126
|
false,
|
|
129
127
|
)}`,
|
|
@@ -190,45 +188,6 @@ cat /etc/default/keyboard`,
|
|
|
190
188
|
logger.info('Build', `${nfsHostToolsPath}/device_scan.sh`);
|
|
191
189
|
fs.copySync(`${underpostRoot}/scripts/device-scan.sh`, `${nfsHostToolsPath}/device_scan.sh`);
|
|
192
190
|
|
|
193
|
-
// Build and write the MAAS enlistment script.
|
|
194
|
-
logger.info('Build', `${nfsHostToolsPath}/enlistment.sh`);
|
|
195
|
-
fs.writeFileSync(
|
|
196
|
-
`${nfsHostToolsPath}/enlistment.sh`,
|
|
197
|
-
`#!/bin/bash
|
|
198
|
-
set -x
|
|
199
|
-
|
|
200
|
-
# ------------------------------------------------------------
|
|
201
|
-
# Step: Commission a machine in MAAS using OAuth1 authentication
|
|
202
|
-
# ------------------------------------------------------------
|
|
203
|
-
|
|
204
|
-
MACHINE_ID=$(cat /underpost/system-id)
|
|
205
|
-
CONSUMER_KEY=$(cat /underpost/consumer-key)
|
|
206
|
-
TOKEN_KEY=$(cat /underpost/token-key)
|
|
207
|
-
TOKEN_SECRET=$(cat /underpost/token-secret)
|
|
208
|
-
|
|
209
|
-
echo ">>> Starting MAAS machine commissioning for system_id: $MACHINE_ID …"
|
|
210
|
-
|
|
211
|
-
curl -X POST \\
|
|
212
|
-
--fail --location --verbose --include --raw --trace-ascii /dev/stdout\\
|
|
213
|
-
--header "Authorization:\\
|
|
214
|
-
OAuth oauth_version=1.0,\\
|
|
215
|
-
oauth_signature_method=PLAINTEXT,\\
|
|
216
|
-
oauth_consumer_key=$CONSUMER_KEY,\\
|
|
217
|
-
oauth_token=$TOKEN_KEY,\\
|
|
218
|
-
oauth_signature=&$TOKEN_SECRET,\\
|
|
219
|
-
oauth_nonce=$(uuidgen),\\
|
|
220
|
-
oauth_timestamp=$(date +%s)"\\
|
|
221
|
-
-F "commissioning_scripts=20-maas-01-install-lldpd"\\
|
|
222
|
-
-F "enable_ssh=1"\\
|
|
223
|
-
-F "skip_bmc_config=1"\\
|
|
224
|
-
-F "skip_networking=1"\\
|
|
225
|
-
-F "skip_storage=1"\\
|
|
226
|
-
-F "testing_scripts=none"\\
|
|
227
|
-
http://${callbackMetaData.runnerHost.ip}:5240/MAAS/api/2.0/machines/$MACHINE_ID/op-commission \\
|
|
228
|
-
2>&1 | tee /underpost/enlistment.log || echo "ERROR: MAAS commissioning returned code $?"`,
|
|
229
|
-
'utf8',
|
|
230
|
-
);
|
|
231
|
-
|
|
232
191
|
// Import SSH keys for root user.
|
|
233
192
|
logger.info('Import ssh keys');
|
|
234
193
|
shellExec(`sudo rm -rf ${nfsHostPath}/root/.ssh`);
|
|
@@ -659,7 +618,30 @@ curl -X POST \\
|
|
|
659
618
|
}
|
|
660
619
|
|
|
661
620
|
return yaml.join('\n');
|
|
662
|
-
}
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* @method httpServerStaticFactory
|
|
625
|
+
* @description Writes cloud-init files (user-data, meta-data, vendor-data) to the bootstrap HTTP server path.
|
|
626
|
+
* @param {object} params
|
|
627
|
+
* @param {string} params.bootstrapHttpServerPath
|
|
628
|
+
* @param {string} params.hostname
|
|
629
|
+
* @param {string} params.cloudConfigSrc
|
|
630
|
+
* @param {string} [params.vendorData='']
|
|
631
|
+
* @memberof UnderpostCloudInit
|
|
632
|
+
* @returns {void}
|
|
633
|
+
*/
|
|
634
|
+
httpServerStaticFactory({ bootstrapHttpServerPath, hostname, cloudConfigSrc, vendorData = '' }) {
|
|
635
|
+
if (!cloudConfigSrc) return;
|
|
636
|
+
const dir = `${bootstrapHttpServerPath}/${hostname}/cloud-init`;
|
|
637
|
+
shellExec(`mkdir -p ${dir}`);
|
|
638
|
+
fs.writeFileSync(`${dir}/user-data`, cloudConfigSrc, 'utf8');
|
|
639
|
+
fs.writeFileSync(`${dir}/meta-data`, `instance-id: ${hostname}\nlocal-hostname: ${hostname}`, 'utf8');
|
|
640
|
+
fs.writeFileSync(`${dir}/vendor-data`, vendorData, 'utf8');
|
|
641
|
+
logger.info(`Cloud-init files written to ${dir}`);
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
/**
|
|
663
645
|
* @method kernelParamsFactory
|
|
664
646
|
* @description Generates the kernel parameters for the target machine's bootloader configuration,
|
|
665
647
|
* including the necessary parameters to enable cloud-init with a specific configuration URL and logging settings.
|
|
@@ -670,7 +652,7 @@ curl -X POST \\
|
|
|
670
652
|
* @param {string} [options.machine.system_id] - The unique identifier of the machine, used to fetch the correct cloud-init preseed configuration from MAAS.
|
|
671
653
|
* @return {array} The modified array of kernel parameters with cloud-init parameters included.
|
|
672
654
|
* @memberof UnderpostCloudInit
|
|
673
|
-
|
|
655
|
+
*/
|
|
674
656
|
kernelParamsFactory(
|
|
675
657
|
macAddress,
|
|
676
658
|
cmd = [],
|
package/src/cli/index.js
CHANGED
|
@@ -76,11 +76,20 @@ program
|
|
|
76
76
|
.option('--info', 'Displays information about available commit types.')
|
|
77
77
|
.option('--diff', 'Shows the current git diff changes.')
|
|
78
78
|
.option('--edit', 'Edit last commit.')
|
|
79
|
-
.option('--msg <msg>', 'Sets a custom commit message.')
|
|
80
79
|
.option('--deploy-id <deploy-id>', 'Sets the deployment configuration ID for the commit context.')
|
|
81
80
|
.option('--cached', 'Commit staged changes only or context.')
|
|
82
81
|
.option('--hashes <hashes>', 'Comma-separated list of specific file hashes of commits.')
|
|
83
82
|
.option('--extension <extension>', 'specific file extensions of commits.')
|
|
83
|
+
.option(
|
|
84
|
+
'--changelog [latest-n]',
|
|
85
|
+
'Print plain the changelog of the specified number of latest n commits, if no number is provided it will get the changelog to latest ci integration',
|
|
86
|
+
)
|
|
87
|
+
.option('--changelog-build', 'Builds a CHANGELOG.md file based on the commit history')
|
|
88
|
+
.option('--changelog-min-version <version>', 'Sets the minimum version limit for --changelog-build (default: 2.85.0)')
|
|
89
|
+
.option(
|
|
90
|
+
'--changelog-no-hash',
|
|
91
|
+
'Excludes commit hashes from the generated changelog entries (used with --changelog-build).',
|
|
92
|
+
)
|
|
84
93
|
.description('Manages commits to a GitHub repository, supporting various commit types and options.')
|
|
85
94
|
.action(Underpost.repo.commit);
|
|
86
95
|
|
|
@@ -150,6 +159,11 @@ program
|
|
|
150
159
|
.option('--dir <dir>', 'HTML dir attribute (default: ltr).')
|
|
151
160
|
.option('--dev', 'Sets the development cli context')
|
|
152
161
|
|
|
162
|
+
.option(
|
|
163
|
+
'--run-sv [port]',
|
|
164
|
+
'Start a standalone Express static server to preview the static build (default port: 5000).',
|
|
165
|
+
)
|
|
166
|
+
|
|
153
167
|
.description(`Manages static build of page, bundles, and documentation with comprehensive customization options.`)
|
|
154
168
|
.action(Underpost.static.callback);
|
|
155
169
|
|
|
@@ -402,10 +416,27 @@ program
|
|
|
402
416
|
'[job-list]',
|
|
403
417
|
`A comma-separated list of job IDs. Options: ${Underpost.cron.getJobsIDs()}. Defaults to all available jobs.`,
|
|
404
418
|
)
|
|
405
|
-
.option('--
|
|
406
|
-
.option('--
|
|
407
|
-
.option(
|
|
408
|
-
|
|
419
|
+
.option('--generate-k8s-cronjobs', 'Generates Kubernetes CronJob YAML manifests from cron configuration.')
|
|
420
|
+
.option('--apply', 'Applies generated K8s CronJob manifests to the cluster via kubectl.')
|
|
421
|
+
.option(
|
|
422
|
+
'--setup-start [deploy-id]',
|
|
423
|
+
'Updates deploy-id package.json start script and generates+applies its K8s CronJob manifests.',
|
|
424
|
+
)
|
|
425
|
+
.option('--namespace <namespace>', 'Kubernetes namespace for the CronJob resources (default: "default").')
|
|
426
|
+
.option('--image <image>', 'Custom container image for the CronJob pods.')
|
|
427
|
+
.option('--git', 'Pass --git flag to cron job execution.')
|
|
428
|
+
.option('--cmd <cmd>', 'Optional pre-script commands to run before cron execution.')
|
|
429
|
+
.option('--dev', 'Use local ./ base path instead of global underpost installation.')
|
|
430
|
+
.option('--k3s', 'Use k3s cluster context (apply directly on host).')
|
|
431
|
+
.option('--kind', 'Use kind cluster context (apply via kind-worker container).')
|
|
432
|
+
.option('--kubeadm', 'Use kubeadm cluster context (apply directly on host).')
|
|
433
|
+
.option('--dry-run', 'Preview cron jobs without executing them.')
|
|
434
|
+
.option(
|
|
435
|
+
'--create-job-now',
|
|
436
|
+
'After applying manifests, immediately create a Job from each CronJob (requires --apply).',
|
|
437
|
+
)
|
|
438
|
+
.option('--ssh', 'Execute backup commands via SSH on the remote node instead of locally.')
|
|
439
|
+
.description('Manages cron jobs: execute jobs directly or generate and apply K8s CronJob manifests.')
|
|
409
440
|
.action(Underpost.cron.callback);
|
|
410
441
|
|
|
411
442
|
program
|
|
@@ -481,6 +512,7 @@ program
|
|
|
481
512
|
.option('--status', 'Checks the status of the SSH service.')
|
|
482
513
|
.option('--connect-uri', 'Displays the connection URI.')
|
|
483
514
|
.option('--copy', 'Copies the connection URI to clipboard.')
|
|
515
|
+
.description('Manages SSH credentials and sessions for remote access to cluster nodes or services.')
|
|
484
516
|
.action(Underpost.ssh.callback);
|
|
485
517
|
|
|
486
518
|
program
|
|
@@ -534,7 +566,11 @@ program
|
|
|
534
566
|
.option('--expose', 'Enables service exposure for the runner execution.')
|
|
535
567
|
.option('--conf-server-path <conf-server-path>', 'Sets a custom configuration server path.')
|
|
536
568
|
.option('--underpost-root <underpost-root>', 'Sets a custom Underpost root path.')
|
|
537
|
-
.option('--cron-jobs <jobs>', '
|
|
569
|
+
.option('--cmd-cron-jobs <cmd-cron-jobs>', 'Pre-script commands to run before cron job execution.')
|
|
570
|
+
.option(
|
|
571
|
+
'--deploy-id-cron-jobs <deploy-id-cron-jobs>',
|
|
572
|
+
'Specifies deployment IDs to synchronize cron jobs with during execution.',
|
|
573
|
+
)
|
|
538
574
|
.option('--timezone <timezone>', 'Sets the timezone for the runner execution.')
|
|
539
575
|
.option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
|
|
540
576
|
.option('--k3s', 'Sets the k3s cluster context for the runner execution.')
|
|
@@ -567,6 +603,11 @@ program
|
|
|
567
603
|
'--monitor-status-max-attempts <attempts>',
|
|
568
604
|
'Sets the maximum number of status check attempts (default: 600).',
|
|
569
605
|
)
|
|
606
|
+
.option('--dry-run', 'Preview operations without executing them.')
|
|
607
|
+
.option(
|
|
608
|
+
'--create-job-now',
|
|
609
|
+
'After applying cron manifests, immediately create a Job from each CronJob (forwarded to cron runner).',
|
|
610
|
+
)
|
|
570
611
|
.description('Runs specified scripts using various runners.')
|
|
571
612
|
.action(Underpost.run.callback);
|
|
572
613
|
|
|
@@ -650,6 +691,10 @@ program
|
|
|
650
691
|
.option('--remove-machines <system-ids>', 'Removes baremetal machines by comma-separated system IDs, or use "all"')
|
|
651
692
|
.option('--clear-discovered', 'Clears all discovered baremetal machines from the database.')
|
|
652
693
|
.option('--commission', 'Init workflow for commissioning a physical machine.')
|
|
694
|
+
.option(
|
|
695
|
+
'--bootstrap-http-server-run',
|
|
696
|
+
'Runs a temporary bootstrap HTTP server for generic purposes such as serving iPXE scripts or ISO images during commissioning.',
|
|
697
|
+
)
|
|
653
698
|
.option(
|
|
654
699
|
'--bootstrap-http-server-path <path>',
|
|
655
700
|
'Sets a custom bootstrap HTTP server path for baremetal commissioning.',
|
|
@@ -661,6 +706,7 @@ program
|
|
|
661
706
|
.option('--iso-url <url>', 'Uses a custom ISO URL for baremetal machine commissioning.')
|
|
662
707
|
.option('--nfs-build', 'Builds an NFS root filesystem for a workflow id config architecture using QEMU emulation.')
|
|
663
708
|
.option('--nfs-mount', 'Mounts the NFS root filesystem for a workflow id config architecture.')
|
|
709
|
+
.option('--nfs-reset', 'Resets the NFS server completely, closing all connections before reloading exports.')
|
|
664
710
|
.option('--nfs-unmount', 'Unmounts the NFS root filesystem for a workflow id config architecture.')
|
|
665
711
|
.option('--nfs-build-server', 'Builds the NFS server for a workflow id config architecture.')
|
|
666
712
|
.option('--nfs-sh', 'Copies QEMU emulation root entrypoint shell command to the clipboard.')
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kickstart configuration generator for Underpost Engine
|
|
3
|
+
* @module src/cli/kickstart.js
|
|
4
|
+
* @namespace UnderpostKickStart
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { loggerFactory } from '../server/logger.js';
|
|
11
|
+
|
|
12
|
+
const logger = loggerFactory(import.meta);
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
class UnderpostKickStart {
|
|
17
|
+
static API = {
|
|
18
|
+
/**
|
|
19
|
+
* @method kickstartHeader
|
|
20
|
+
* @description Generates the kickstart header section with template variables.
|
|
21
|
+
* @param {object} options
|
|
22
|
+
* @param {string} [options.lang='en_US.UTF-8']
|
|
23
|
+
* @param {string} [options.keyboard='us']
|
|
24
|
+
* @param {string} [options.timezone='America/New_York']
|
|
25
|
+
* @param {string} [options.rootPassword]
|
|
26
|
+
* @memberof UnderpostKickStart
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
kickstartHeader: ({ lang = 'en_US.UTF-8', keyboard = 'us', timezone = 'America/New_York', rootPassword = '' }) => {
|
|
30
|
+
return [
|
|
31
|
+
'# Rocky Linux 9 Kickstart - Ephemeral Anaconda Live Environment with SSHD',
|
|
32
|
+
'cmdline',
|
|
33
|
+
'eula --agreed',
|
|
34
|
+
`keyboard --vckeymap=${keyboard} --xlayouts='${keyboard}'`,
|
|
35
|
+
`lang ${lang}`,
|
|
36
|
+
'network --bootproto=dhcp --device=link --activate --onboot=yes',
|
|
37
|
+
`timezone ${timezone} --utc`,
|
|
38
|
+
rootPassword ? `rootpw --plaintext ${rootPassword}` : 'rootpw --lock',
|
|
39
|
+
'firstboot --disable',
|
|
40
|
+
'skipx',
|
|
41
|
+
].join('\n');
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @method kickstartPreVariables
|
|
46
|
+
* @description Generates the variable assignments block for the %pre script.
|
|
47
|
+
* @param {object} options
|
|
48
|
+
* @param {string} [options.rootPassword]
|
|
49
|
+
* @param {string} [options.authorizedKeys]
|
|
50
|
+
* @param {string} [options.adminUsername]
|
|
51
|
+
* @memberof UnderpostKickStart
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
kickstartPreVariables: ({ rootPassword = '', authorizedKeys = '', adminUsername = '' }) => {
|
|
55
|
+
const sanitizedKeys = (authorizedKeys || '').trim();
|
|
56
|
+
return [
|
|
57
|
+
`ROOT_PASS='${rootPassword || ''}'`,
|
|
58
|
+
`AUTHORIZED_KEYS='${sanitizedKeys}'`,
|
|
59
|
+
`ADMIN_USER='${adminUsername || process.env.MAAS_ADMIN_USERNAME || 'maas'}'`,
|
|
60
|
+
].join('\n');
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @method kickstartFactory
|
|
65
|
+
* @description Generates a complete kickstart configuration by combining the header,
|
|
66
|
+
* variable assignments, and the rocky-kickstart.sh script body.
|
|
67
|
+
* @param {object} options
|
|
68
|
+
* @param {string} [options.lang='en_US.UTF-8']
|
|
69
|
+
* @param {string} [options.keyboard='us']
|
|
70
|
+
* @param {string} [options.timezone='America/New_York']
|
|
71
|
+
* @param {string} [options.rootPassword]
|
|
72
|
+
* @param {string} [options.authorizedKeys]
|
|
73
|
+
* @memberof UnderpostKickStart
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
kickstartFactory: ({
|
|
77
|
+
lang = 'en_US.UTF-8',
|
|
78
|
+
keyboard = 'us',
|
|
79
|
+
timezone = 'America/New_York',
|
|
80
|
+
rootPassword = process.env.MAAS_ADMIN_PASS,
|
|
81
|
+
authorizedKeys = '',
|
|
82
|
+
}) => {
|
|
83
|
+
const adminUsername = process.env.MAAS_ADMIN_USERNAME || 'maas';
|
|
84
|
+
const header = UnderpostKickStart.API.kickstartHeader({ lang, keyboard, timezone, rootPassword });
|
|
85
|
+
const variables = UnderpostKickStart.API.kickstartPreVariables({ rootPassword, authorizedKeys, adminUsername });
|
|
86
|
+
|
|
87
|
+
const scriptPath = path.resolve(__dirname, '../../scripts/rocky-kickstart.sh');
|
|
88
|
+
const scriptBody = fs.readFileSync(scriptPath, 'utf8');
|
|
89
|
+
|
|
90
|
+
return [
|
|
91
|
+
header,
|
|
92
|
+
'',
|
|
93
|
+
'%pre --interpreter=/bin/bash --log=/tmp/ks-pre.log --erroronfail',
|
|
94
|
+
'#!/bin/bash',
|
|
95
|
+
variables,
|
|
96
|
+
'',
|
|
97
|
+
scriptBody,
|
|
98
|
+
'%end',
|
|
99
|
+
].join('\n');
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @method kernelParamsFactory
|
|
104
|
+
* @description Appends kickstart-specific kernel parameters (inst.ks, inst.repo, inst.text, inst.sshd).
|
|
105
|
+
* @param {string} macAddress - The MAC address of the target machine.
|
|
106
|
+
* @param {array} cmd - The existing array of kernel parameters.
|
|
107
|
+
* @param {object} options - Options for generating kernel parameters.
|
|
108
|
+
* @param {string} options.ipDhcpServer - The IP address of the DHCP server.
|
|
109
|
+
* @param {number} [options.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server.
|
|
110
|
+
* @param {string} options.hostname - The hostname of the target machine.
|
|
111
|
+
* @param {string} [options.architecture='amd64'] - The target architecture.
|
|
112
|
+
* @memberof UnderpostKickStart
|
|
113
|
+
* @returns {array}
|
|
114
|
+
*/
|
|
115
|
+
kernelParamsFactory(
|
|
116
|
+
macAddress,
|
|
117
|
+
cmd = [],
|
|
118
|
+
options = { ipDhcpServer: '', bootstrapHttpServerPort: 8888, hostname: '', architecture: 'amd64' },
|
|
119
|
+
) {
|
|
120
|
+
const { ipDhcpServer, bootstrapHttpServerPort, hostname, architecture } = options;
|
|
121
|
+
const repoArch = architecture && architecture.match('arm64') ? 'aarch64' : 'x86_64';
|
|
122
|
+
return cmd.concat([
|
|
123
|
+
`inst.ks=http://${ipDhcpServer}:${bootstrapHttpServerPort}/${hostname}/ks.cfg`,
|
|
124
|
+
`inst.repo=http://dl.rockylinux.org/pub/rocky/9/BaseOS/${repoArch}/os/`,
|
|
125
|
+
`inst.text`,
|
|
126
|
+
`inst.sshd`,
|
|
127
|
+
]);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @method httpServerStaticFactory
|
|
132
|
+
* @description Writes kickstart ks.cfg file to the bootstrap HTTP server path.
|
|
133
|
+
* @param {object} params
|
|
134
|
+
* @param {string} params.bootstrapHttpServerPath
|
|
135
|
+
* @param {string} params.hostname
|
|
136
|
+
* @param {string} params.kickstartSrc
|
|
137
|
+
* @memberof UnderpostKickStart
|
|
138
|
+
* @returns {void}
|
|
139
|
+
*/
|
|
140
|
+
httpServerStaticFactory({ bootstrapHttpServerPath, hostname, kickstartSrc }) {
|
|
141
|
+
if (!kickstartSrc) return;
|
|
142
|
+
const dest = `${bootstrapHttpServerPath}/${hostname}/ks.cfg`;
|
|
143
|
+
fs.writeFileSync(dest, kickstartSrc, 'utf8');
|
|
144
|
+
logger.info(`Kickstart file written to ${dest}`);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default UnderpostKickStart;
|
package/src/cli/repository.js
CHANGED
|
@@ -88,10 +88,13 @@ class UnderpostRepository {
|
|
|
88
88
|
* @param {boolean} [options.cached=false] - If true, commits only staged changes.
|
|
89
89
|
* @param {number} [options.log=0] - If greater than 0, shows the last N commits with diffs.
|
|
90
90
|
* @param {boolean} [options.lastMsg=0] - If greater than 0, copies or show the last last single n commit message to clipboard.
|
|
91
|
-
* @param {string} [options.msg=''] - If provided, outputs this message instead of committing.
|
|
92
91
|
* @param {string} [options.deployId=''] - An optional deploy ID to include in the commit message.
|
|
93
92
|
* @param {string} [options.hashes=''] - If provided with diff option, shows the diff between two hashes.
|
|
94
93
|
* @param {string} [options.extension=''] - If provided with diff option, filters the diff by this file extension.
|
|
94
|
+
* @param {boolean|string} [options.changelog=undefined] - If true, prints the changelog since the last CI integration commit (starting with 'ci(package-pwa-microservices-'). If a number string, prints the changelog of the last N commits split by version sections. Only considers commits starting with '[<tag>]'.
|
|
95
|
+
* @param {boolean} [options.changelogBuild=false] - If true, scrapes all git history and builds a full CHANGELOG.md. Commits containing 'New release v:' are used as version section titles. Only commits starting with '[<tag>]' are included as entries.
|
|
96
|
+
* @param {string} [options.changelogMinVersion=''] - If set, overrides the default minimum version limit (2.85.0) for --changelog-build.
|
|
97
|
+
* @param {boolean} [options.changelogNoHash=false] - If true, omits commit hashes from the changelog entries.
|
|
95
98
|
* @memberof UnderpostRepository
|
|
96
99
|
*/
|
|
97
100
|
commit(
|
|
@@ -108,13 +111,169 @@ class UnderpostRepository {
|
|
|
108
111
|
cached: false,
|
|
109
112
|
lastMsg: 0,
|
|
110
113
|
log: 0,
|
|
111
|
-
msg: '',
|
|
112
114
|
deployId: '',
|
|
113
115
|
hashes: '',
|
|
114
116
|
extension: '',
|
|
117
|
+
changelog: undefined,
|
|
118
|
+
changelogBuild: false,
|
|
119
|
+
changelogMinVersion: '',
|
|
120
|
+
changelogNoHash: false,
|
|
115
121
|
},
|
|
116
122
|
) {
|
|
117
123
|
if (!repoPath) repoPath = '.';
|
|
124
|
+
|
|
125
|
+
if (options.changelog !== undefined || options.changelogBuild) {
|
|
126
|
+
const ciIntegrationPrefix = 'ci(package-pwa-microservices-';
|
|
127
|
+
const releaseMatch = 'New release v:';
|
|
128
|
+
|
|
129
|
+
// Helper: parse [<tag>] commits into grouped sections
|
|
130
|
+
const buildSectionChangelog = (commits) => {
|
|
131
|
+
const groups = {};
|
|
132
|
+
const tagOrder = [];
|
|
133
|
+
for (const commit of commits) {
|
|
134
|
+
if (!commit.message.startsWith('[')) continue;
|
|
135
|
+
const match = commit.message.match(/^\[([^\]]+)\]\s*(.*)/);
|
|
136
|
+
if (match) {
|
|
137
|
+
const tag = match[1].trim();
|
|
138
|
+
const context = match[2].trim().replaceAll('"', '');
|
|
139
|
+
if (!groups[tag]) {
|
|
140
|
+
groups[tag] = [];
|
|
141
|
+
tagOrder.push(tag);
|
|
142
|
+
}
|
|
143
|
+
groups[tag].push({ ...commit, context });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
let out = '';
|
|
147
|
+
for (const tag of tagOrder) {
|
|
148
|
+
out += `### ${tag}\n\n`;
|
|
149
|
+
for (const entry of groups[tag]) {
|
|
150
|
+
out += `- ${entry.context}${options.changelogNoHash ? '' : ` (${commitUrl(entry.hash, entry.fullHash)})`}\n`;
|
|
151
|
+
}
|
|
152
|
+
out += '\n';
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Helper: fetch git log as structured array
|
|
158
|
+
const fetchHistory = (limit) => {
|
|
159
|
+
const limitArg = limit ? ` -n ${limit}` : '';
|
|
160
|
+
const rawLog = shellExec(`git log --pretty=format:"%h||%H||%s||%ci"${limitArg}`, {
|
|
161
|
+
stdout: true,
|
|
162
|
+
silent: true,
|
|
163
|
+
disableLog: true,
|
|
164
|
+
});
|
|
165
|
+
return rawLog
|
|
166
|
+
.split('\n')
|
|
167
|
+
.map((line) => {
|
|
168
|
+
const parts = line.split('||');
|
|
169
|
+
return {
|
|
170
|
+
hash: (parts[0] || '').trim(),
|
|
171
|
+
fullHash: (parts[1] || '').trim(),
|
|
172
|
+
message: parts[2] || '',
|
|
173
|
+
date: parts[3] || '',
|
|
174
|
+
};
|
|
175
|
+
})
|
|
176
|
+
.filter((c) => c.hash);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const githubUser = process.env.GITHUB_USERNAME || 'underpostnet';
|
|
180
|
+
const commitUrl = (shortHash, fullHash) =>
|
|
181
|
+
`[${shortHash}](https://github.com/${githubUser}/engine/commit/${fullHash})`;
|
|
182
|
+
|
|
183
|
+
// Helper: extract version from commit message containing 'New release v:'
|
|
184
|
+
const extractVersion = (message) => {
|
|
185
|
+
const idx = message.indexOf(releaseMatch);
|
|
186
|
+
if (idx === -1) return null;
|
|
187
|
+
return message.substring(idx + releaseMatch.length).trim();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Helper: split commits array into version sections by 'New release v:' boundary
|
|
191
|
+
const buildVersionSections = (commits) => {
|
|
192
|
+
const sections = [];
|
|
193
|
+
let currentSection = { title: null, date: new Date().toISOString().split('T')[0], commits: [] };
|
|
194
|
+
|
|
195
|
+
for (const commit of commits) {
|
|
196
|
+
const version = extractVersion(commit.message);
|
|
197
|
+
if (version) {
|
|
198
|
+
// Push accumulated commits as a section
|
|
199
|
+
sections.push(currentSection);
|
|
200
|
+
// Start new version section; commits below this one belong to it
|
|
201
|
+
const commitDate = commit.date ? commit.date.split(' ')[0] : '';
|
|
202
|
+
currentSection = { title: `${releaseMatch}${version}`, date: commitDate, hash: commit.hash, commits: [] };
|
|
203
|
+
} else {
|
|
204
|
+
currentSection.commits.push(commit);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Push the last (oldest) section
|
|
208
|
+
if (currentSection.commits.length > 0) sections.push(currentSection);
|
|
209
|
+
return sections;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Helper: render sections array into changelog markdown string
|
|
213
|
+
const renderSections = (sections) => {
|
|
214
|
+
let changelog = '';
|
|
215
|
+
for (const section of sections) {
|
|
216
|
+
const sectionBody = buildSectionChangelog(section.commits);
|
|
217
|
+
if (!sectionBody) continue;
|
|
218
|
+
if (section.title) {
|
|
219
|
+
changelog += `## ${section.title}${options.changelogNoHash ? '' : ` (${section.date})`}\n\n`;
|
|
220
|
+
} else {
|
|
221
|
+
changelog += `## ${section.date}\n\n`;
|
|
222
|
+
}
|
|
223
|
+
changelog += sectionBody;
|
|
224
|
+
}
|
|
225
|
+
return changelog;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const changelogMinVersion = options.changelogMinVersion || '2.97.1';
|
|
229
|
+
|
|
230
|
+
if (options.changelogBuild) {
|
|
231
|
+
// --changelog-build: scrape ALL history, split by 'New release v:' commits as version sections
|
|
232
|
+
const allCommits = fetchHistory();
|
|
233
|
+
const sections = buildVersionSections(allCommits);
|
|
234
|
+
|
|
235
|
+
// Filter sections: stop at changelogMinVersion boundary
|
|
236
|
+
const limitedSections = [];
|
|
237
|
+
for (const section of sections) {
|
|
238
|
+
limitedSections.push(section);
|
|
239
|
+
if (section.title) {
|
|
240
|
+
const versionStr = section.title.replace(releaseMatch, '').trim();
|
|
241
|
+
if (versionStr === changelogMinVersion) break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let changelog = renderSections(limitedSections);
|
|
246
|
+
|
|
247
|
+
if (!changelog) {
|
|
248
|
+
changelog = `No changelog entries found.\n`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const changelogPath = `${repoPath === '.' ? '.' : repoPath}/CHANGELOG.md`;
|
|
252
|
+
fs.writeFileSync(changelogPath, `# Changelog\n\n${changelog}`);
|
|
253
|
+
logger.info('CHANGELOG.md built at', changelogPath);
|
|
254
|
+
} else {
|
|
255
|
+
// --changelog [latest-n]: print changelog of last N commits or since last release
|
|
256
|
+
const hasExplicitCount =
|
|
257
|
+
options.changelog !== undefined && options.changelog !== true && !isNaN(parseInt(options.changelog));
|
|
258
|
+
const scanLimit = hasExplicitCount ? parseInt(options.changelog) : 500;
|
|
259
|
+
const allCommits = fetchHistory(scanLimit);
|
|
260
|
+
|
|
261
|
+
let commits;
|
|
262
|
+
if (!hasExplicitCount) {
|
|
263
|
+
// No explicit count: find commits up to the last CI integration boundary
|
|
264
|
+
const ciIndex = allCommits.findIndex((c) => c.message.startsWith(ciIntegrationPrefix));
|
|
265
|
+
commits = ciIndex >= 0 ? allCommits.slice(0, ciIndex) : allCommits;
|
|
266
|
+
} else {
|
|
267
|
+
commits = allCommits;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const sections = buildVersionSections(commits);
|
|
271
|
+
let changelog = renderSections(sections);
|
|
272
|
+
console.log(changelog || `No changelog entries found.\n`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
118
277
|
if (options.diff && options.hashes) {
|
|
119
278
|
const hashes = options.hashes.split(',');
|
|
120
279
|
const cmd = `git --no-pager diff ${hashes[0]} ${hashes[1] ? hashes[1] : 'HEAD'}${options.extension ? ` -- '*.${options.extension}'` : ''}`;
|
|
@@ -123,16 +282,6 @@ class UnderpostRepository {
|
|
|
123
282
|
} else console.log(cmd);
|
|
124
283
|
return;
|
|
125
284
|
}
|
|
126
|
-
if (options.msg) {
|
|
127
|
-
options.msg = options.msg.replaceAll('"', '').replaceAll(`'`, '').replaceAll('`', '');
|
|
128
|
-
let key = Object.keys(commitData).find((k) => k && options.msg.toLocaleLowerCase().slice(0, 16).match(k));
|
|
129
|
-
if (!key) key = Object.keys(commitData).find((k) => k && options.msg.toLocaleLowerCase().match(k));
|
|
130
|
-
if (!key || key === undefined) key = 'chore';
|
|
131
|
-
shellExec(
|
|
132
|
-
`underpost cmt ${repoPath} ${key} ${options.deployId ? options.deployId : `''`} '${options.msg.replaceAll(`${key}(${key}`, '')}'`,
|
|
133
|
-
);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
285
|
if (options.lastMsg) {
|
|
137
286
|
if (options.copy) {
|
|
138
287
|
pbcopy(Underpost.repo.getLastCommitMsg(options.lastMsg - 1));
|
|
@@ -197,7 +346,11 @@ class UnderpostRepository {
|
|
|
197
346
|
* @memberof UnderpostRepository
|
|
198
347
|
*/
|
|
199
348
|
getLastCommitMsg(skip = 0) {
|
|
200
|
-
return shellExec(`git --no-pager log -1 --skip=${skip} --pretty=%B`, {
|
|
349
|
+
return shellExec(`git --no-pager log -1 --skip=${skip} --pretty=%B`, {
|
|
350
|
+
stdout: true,
|
|
351
|
+
silent: true,
|
|
352
|
+
disableLog: true,
|
|
353
|
+
});
|
|
201
354
|
},
|
|
202
355
|
|
|
203
356
|
/**
|