@underpostnet/cyberia 3.2.80
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/.dockerignore +14 -0
- package/.env.example +144 -0
- package/.github/workflows/cyberia-client.cd.yml +40 -0
- package/.github/workflows/cyberia-server.cd.yml +40 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +71 -0
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +70 -0
- package/.github/workflows/docker-image.cyberia-server.ci.yml +71 -0
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +71 -0
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +53 -0
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +53 -0
- package/.github/workflows/engine-cyberia.cd.yml +157 -0
- package/.github/workflows/engine-cyberia.ci.yml +125 -0
- package/.github/workflows/ghpkg.ci.yml +160 -0
- package/.github/workflows/gitlab.ci.yml +20 -0
- package/.github/workflows/hardhat.ci.yml +82 -0
- package/.github/workflows/npmpkg.ci.yml +126 -0
- package/.github/workflows/publish.ci.yml +87 -0
- package/.github/workflows/publish.cyberia.ci.yml +79 -0
- package/.github/workflows/pwa-microservices-template-page.cd.yml +65 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +33 -0
- package/.github/workflows/release.cd.yml +46 -0
- package/.nycrc +9 -0
- package/.prettierignore +12 -0
- package/.prettierrc +9 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +26 -0
- package/CHANGELOG.md +2516 -0
- package/CLI-HELP.md +1099 -0
- package/Dockerfile +161 -0
- package/Dockerfile.dev +161 -0
- package/Dockerfile.test +165 -0
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/baremetal/commission-workflows.json +211 -0
- package/baremetal/packer-workflows.json +24 -0
- package/bin/build.js +261 -0
- package/bin/build.template.js +33 -0
- package/bin/cyberia.js +5627 -0
- package/bin/deploy.js +1547 -0
- package/bin/index.js +5627 -0
- package/bump.config.js +27 -0
- package/compose.env +144 -0
- package/conf.js +952 -0
- package/deployment.yaml +418 -0
- package/docker-compose.yml +434 -0
- package/examples/static-page/README.md +150 -0
- package/examples/static-page/ssr-components/CustomPage.js +554 -0
- package/examples/static-page/static-config-example.json +57 -0
- package/hardhat/.env.example +31 -0
- package/hardhat/contracts/ObjectLayerToken.sol +391 -0
- package/hardhat/deployments/.gitkeep +0 -0
- package/hardhat/deployments/hardhat-ObjectLayerToken.json +11 -0
- package/hardhat/hardhat.config.js +136 -0
- package/hardhat/ignition/modules/.gitkeep +0 -0
- package/hardhat/ignition/modules/ObjectLayerToken.js +21 -0
- package/hardhat/networks/besu-object-layer.network.json +138 -0
- package/hardhat/package-lock.json +2871 -0
- package/hardhat/package.json +43 -0
- package/hardhat/scripts/deployObjectLayerToken.js +98 -0
- package/hardhat/test/ObjectLayerToken.js +696 -0
- package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
- package/hardhat/types/ethers-contracts/common.ts +92 -0
- package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
- package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
- package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
- package/hardhat/types/ethers-contracts/index.ts +6 -0
- package/ipfs/configure-ipfs.sh +13 -0
- package/jsconfig.json +9 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +48 -0
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +48 -0
- package/manifests/deployment/adminer/deployment.yaml +32 -0
- package/manifests/deployment/adminer/kustomization.yaml +7 -0
- package/manifests/deployment/adminer/service.yaml +13 -0
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +418 -0
- package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
- package/manifests/deployment/dd-cyberia-development/proxy.yaml +189 -0
- package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +164 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +163 -0
- package/manifests/deployment/dd-default-development/proxy.yaml +46 -0
- package/manifests/deployment/fastapi/backend-deployment.yml +120 -0
- package/manifests/deployment/fastapi/backend-service.yml +19 -0
- package/manifests/deployment/fastapi/frontend-deployment.yml +54 -0
- package/manifests/deployment/fastapi/frontend-service.yml +15 -0
- package/manifests/deployment/fastapi/initial_data.sh +8 -0
- package/manifests/deployment/kafka/deployment.yaml +67 -0
- package/manifests/deployment/mongo-express/deployment.yaml +62 -0
- package/manifests/deployment/phpmyadmin/deployment.yaml +54 -0
- package/manifests/deployment/playwright/deployment.yaml +52 -0
- package/manifests/deployment/spark/spark-pi-py.yaml +20 -0
- package/manifests/deployment/tensorflow/tf-gpu-test.yaml +63 -0
- package/manifests/envoy-service-nodeport.yaml +22 -0
- package/manifests/grafana/deployment.yaml +64 -0
- package/manifests/grafana/kustomization.yaml +7 -0
- package/manifests/grafana/pvc.yaml +12 -0
- package/manifests/grafana/service.yaml +14 -0
- package/manifests/grafana/storage-class.yaml +9 -0
- package/manifests/ipfs/configmap.yaml +64 -0
- package/manifests/ipfs/headless-service.yaml +35 -0
- package/manifests/ipfs/kustomization.yaml +8 -0
- package/manifests/ipfs/statefulset.yaml +149 -0
- package/manifests/ipfs/storage-class.yaml +9 -0
- package/manifests/kind-config-dev.yaml +20 -0
- package/manifests/kind-config.yaml +12 -0
- package/manifests/kubeadm-calico-config.yaml +14 -0
- package/manifests/letsencrypt-prod.yaml +14 -0
- package/manifests/lxd/lxd-admin-profile.yaml +26 -0
- package/manifests/lxd/lxd-preseed.yaml +30 -0
- package/manifests/mariadb/kustomization.yaml +9 -0
- package/manifests/mariadb/pv.yaml +12 -0
- package/manifests/mariadb/pvc.yaml +10 -0
- package/manifests/mariadb/service.yaml +10 -0
- package/manifests/mariadb/statefulset.yaml +56 -0
- package/manifests/mariadb/storage-class.yaml +10 -0
- package/manifests/mongodb/headless-service.yaml +10 -0
- package/manifests/mongodb/kustomization.yaml +11 -0
- package/manifests/mongodb/pv-pvc.yaml +59 -0
- package/manifests/mongodb/statefulset.yaml +113 -0
- package/manifests/mongodb/storage-class.yaml +9 -0
- package/manifests/mongodb-4.4/headless-service.yaml +10 -0
- package/manifests/mongodb-4.4/kustomization.yaml +9 -0
- package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
- package/manifests/mongodb-4.4/pv-pvc.yaml +19 -0
- package/manifests/mongodb-4.4/service-deployment.yaml +61 -0
- package/manifests/mongodb-4.4/statefulset.yaml +79 -0
- package/manifests/mongodb-4.4/storage-class.yaml +9 -0
- package/manifests/mysql/kustomization.yaml +7 -0
- package/manifests/mysql/pv-pvc.yaml +27 -0
- package/manifests/mysql/statefulset.yaml +55 -0
- package/manifests/postgresql/configmap.yaml +9 -0
- package/manifests/postgresql/kustomization.yaml +10 -0
- package/manifests/postgresql/pv.yaml +15 -0
- package/manifests/postgresql/pvc.yaml +13 -0
- package/manifests/postgresql/service.yaml +10 -0
- package/manifests/postgresql/statefulset.yaml +37 -0
- package/manifests/prometheus/deployment.yaml +82 -0
- package/manifests/pv-pvc-dd.yaml +34 -0
- package/manifests/valkey/kustomization.yaml +7 -0
- package/manifests/valkey/service.yaml +10 -0
- package/manifests/valkey/statefulset.yaml +37 -0
- package/manifests/valkey/valkey-nodeport.yaml +17 -0
- package/mongodb/entrypoint.sh +76 -0
- package/nginx.conf +128 -0
- package/nodemon.json +6 -0
- package/package.json +101 -0
- 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/proxy.yaml +189 -0
- package/pv-pvc.yaml +164 -0
- package/scripts/device-scan.sh +65 -0
- package/scripts/disk-clean.sh +182 -0
- package/scripts/disk-devices.sh +13 -0
- package/scripts/gen-fqdns.sh +14 -0
- package/scripts/gpu-diag.sh +19 -0
- package/scripts/ip-info.sh +118 -0
- package/scripts/ipxe-setup.sh +200 -0
- package/scripts/k3s-node-setup.sh +124 -0
- package/scripts/kubeadm-node-setup.sh +317 -0
- package/scripts/link-local-underpost-cli.sh +6 -0
- package/scripts/lxd-vm-setup.sh +208 -0
- package/scripts/maas-nat-firewalld.sh +145 -0
- package/scripts/maas-setup.sh +135 -0
- package/scripts/maas-upload-boot-resource.sh +183 -0
- package/scripts/nat-iptables.sh +111 -0
- package/scripts/nvim.sh +91 -0
- package/scripts/packer-init-vars-file.sh +40 -0
- package/scripts/packer-setup.sh +289 -0
- package/scripts/ports-ls.sh +33 -0
- package/scripts/quick-tftp.sh +19 -0
- package/scripts/rhel-grpc-setup.sh +56 -0
- package/scripts/rocky-kickstart.sh +986 -0
- package/scripts/rocky-pwa.sh +200 -0
- package/scripts/rocky-setup.sh +106 -0
- package/scripts/rpmfusion-ffmpeg-setup.sh +32 -0
- package/scripts/ssl.sh +170 -0
- package/scripts/test-monitor.sh +248 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +30 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +155 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +39 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +382 -0
- package/src/api/core/core.controller.js +8 -0
- package/src/api/core/core.model.js +11 -0
- package/src/api/core/core.router.js +23 -0
- package/src/api/core/core.service.js +30 -0
- package/src/api/crypto/crypto.controller.js +10 -0
- package/src/api/crypto/crypto.model.js +23 -0
- package/src/api/crypto/crypto.router.js +23 -0
- package/src/api/crypto/crypto.service.js +64 -0
- package/src/api/cyberia-action/cyberia-action.controller.js +8 -0
- package/src/api/cyberia-action/cyberia-action.model.js +79 -0
- package/src/api/cyberia-action/cyberia-action.router.js +21 -0
- package/src/api/cyberia-action/cyberia-action.service.js +58 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +27 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +100 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +70 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +8 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +20 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +57 -0
- package/src/api/cyberia-entity/cyberia-entity.controller.js +6 -0
- package/src/api/cyberia-entity/cyberia-entity.model.js +25 -0
- package/src/api/cyberia-entity/cyberia-entity.router.js +17 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +6 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +17 -0
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +337 -0
- package/src/api/cyberia-instance/cyberia-instance-boot.service.js +57 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +583 -0
- package/src/api/cyberia-instance/cyberia-instance.controller.js +42 -0
- package/src/api/cyberia-instance/cyberia-instance.model.js +117 -0
- package/src/api/cyberia-instance/cyberia-instance.router.js +75 -0
- package/src/api/cyberia-instance/cyberia-instance.service.js +190 -0
- package/src/api/cyberia-instance/cyberia-portal-connector.js +260 -0
- package/src/api/cyberia-instance/cyberia-world-generator.js +618 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +6 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +200 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +17 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +46 -0
- package/src/api/cyberia-map/cyberia-map.controller.js +6 -0
- package/src/api/cyberia-map/cyberia-map.model.js +33 -0
- package/src/api/cyberia-map/cyberia-map.router.js +19 -0
- package/src/api/cyberia-map/cyberia-map.service.js +76 -0
- package/src/api/cyberia-quest/cyberia-quest.controller.js +9 -0
- package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +26 -0
- package/src/api/cyberia-quest/cyberia-quest.service.js +97 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +6 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +21 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
- package/src/api/cyberia-saga/cyberia-saga.controller.js +6 -0
- package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
- package/src/api/cyberia-saga/cyberia-saga.router.js +17 -0
- package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +1076 -0
- package/src/api/cyberia-skill/cyberia-skill.controller.js +6 -0
- package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
- package/src/api/cyberia-skill/cyberia-skill.router.js +17 -0
- package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
- package/src/api/default/default.controller.js +6 -0
- package/src/api/default/default.model.js +20 -0
- package/src/api/default/default.router.js +21 -0
- package/src/api/default/default.service.js +42 -0
- package/src/api/document/document.controller.js +9 -0
- package/src/api/document/document.model.js +95 -0
- package/src/api/document/document.router.js +32 -0
- package/src/api/document/document.service.js +633 -0
- package/src/api/file/file.controller.js +15 -0
- package/src/api/file/file.model.js +127 -0
- package/src/api/file/file.ref.json +39 -0
- package/src/api/file/file.router.js +36 -0
- package/src/api/file/file.service.js +499 -0
- package/src/api/instance/instance.controller.js +8 -0
- package/src/api/instance/instance.model.js +34 -0
- package/src/api/instance/instance.router.js +27 -0
- package/src/api/instance/instance.service.js +75 -0
- package/src/api/ipfs/ipfs.controller.js +8 -0
- package/src/api/ipfs/ipfs.model.js +77 -0
- package/src/api/ipfs/ipfs.router.js +26 -0
- package/src/api/ipfs/ipfs.service.js +145 -0
- package/src/api/object-layer/object-layer.controller.js +18 -0
- package/src/api/object-layer/object-layer.model.js +200 -0
- package/src/api/object-layer/object-layer.router.js +729 -0
- package/src/api/object-layer/object-layer.service.js +894 -0
- package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +6 -0
- package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +93 -0
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +17 -0
- package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +46 -0
- package/src/api/test/test.controller.js +14 -0
- package/src/api/test/test.model.js +14 -0
- package/src/api/test/test.router.js +23 -0
- package/src/api/test/test.service.js +35 -0
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +100 -0
- package/src/api/user/postman_collection.json +216 -0
- package/src/api/user/user.build.js +16 -0
- package/src/api/user/user.controller.js +35 -0
- package/src/api/user/user.model.js +142 -0
- package/src/api/user/user.router.js +367 -0
- package/src/api/user/user.service.js +582 -0
- package/src/api.js +23 -0
- package/src/cli/baremetal.js +3947 -0
- package/src/cli/cloud-init.js +673 -0
- package/src/cli/cluster.js +1395 -0
- package/src/cli/db.js +1533 -0
- package/src/cli/deploy.js +1542 -0
- package/src/cli/docker-compose.js +802 -0
- package/src/cli/env.js +177 -0
- package/src/cli/fs.js +380 -0
- package/src/cli/image.js +389 -0
- package/src/cli/index.js +1059 -0
- package/src/cli/ipfs.js +182 -0
- package/src/cli/kickstart.js +271 -0
- package/src/cli/kubectl.js +214 -0
- package/src/cli/lxd.js +1232 -0
- package/src/cli/monitor.js +659 -0
- package/src/cli/release.js +595 -0
- package/src/cli/repository.js +1969 -0
- package/src/cli/run.js +3326 -0
- package/src/cli/secrets.js +151 -0
- package/src/cli/ssh.js +975 -0
- package/src/cli/static.js +653 -0
- package/src/cli/system.js +332 -0
- package/src/cli/test.js +176 -0
- package/src/client/Cryptokoyn.index.js +30 -0
- package/src/client/CyberiaPortal.index.js +31 -0
- package/src/client/Default.index.js +77 -0
- package/src/client/Itemledger.index.js +35 -0
- package/src/client/Underpost.index.js +31 -0
- package/src/client/components/core/404.js +20 -0
- package/src/client/components/core/500.js +20 -0
- package/src/client/components/core/Account.js +630 -0
- package/src/client/components/core/AgGrid.js +238 -0
- package/src/client/components/core/Alert.js +76 -0
- package/src/client/components/core/AppStore.js +69 -0
- package/src/client/components/core/Auth.js +353 -0
- package/src/client/components/core/Badge.js +26 -0
- package/src/client/components/core/Blockchain.js +41 -0
- package/src/client/components/core/Blog.js +9 -0
- package/src/client/components/core/BtnIcon.js +109 -0
- package/src/client/components/core/CalendarCore.js +437 -0
- package/src/client/components/core/Chat.js +62 -0
- package/src/client/components/core/ClientEvents.js +163 -0
- package/src/client/components/core/ColorPaletteElement.js +309 -0
- package/src/client/components/core/CommonJs.js +1014 -0
- package/src/client/components/core/Content.js +349 -0
- package/src/client/components/core/Css.js +1134 -0
- package/src/client/components/core/CssCore.js +881 -0
- package/src/client/components/core/D3Chart.js +44 -0
- package/src/client/components/core/Docs.js +478 -0
- package/src/client/components/core/DropDown.js +321 -0
- package/src/client/components/core/EventBus.js +96 -0
- package/src/client/components/core/EventsUI.js +130 -0
- package/src/client/components/core/FileExplorer.js +1406 -0
- package/src/client/components/core/FullScreen.js +201 -0
- package/src/client/components/core/Input.js +537 -0
- package/src/client/components/core/Keyboard.js +80 -0
- package/src/client/components/core/KeyboardAvoidance.js +145 -0
- package/src/client/components/core/LoadingAnimation.js +152 -0
- package/src/client/components/core/LogIn.js +212 -0
- package/src/client/components/core/LogOut.js +72 -0
- package/src/client/components/core/Logger.js +22 -0
- package/src/client/components/core/Modal.js +2956 -0
- package/src/client/components/core/NotificationManager.js +80 -0
- package/src/client/components/core/Pagination.js +252 -0
- package/src/client/components/core/Panel.js +1308 -0
- package/src/client/components/core/PanelForm.js +915 -0
- package/src/client/components/core/Polyhedron.js +947 -0
- package/src/client/components/core/PublicProfile.js +895 -0
- package/src/client/components/core/Recover.js +211 -0
- package/src/client/components/core/Responsive.js +146 -0
- package/src/client/components/core/RichText.js +33 -0
- package/src/client/components/core/Router.js +523 -0
- package/src/client/components/core/Scroll.js +76 -0
- package/src/client/components/core/SearchBox.js +1007 -0
- package/src/client/components/core/SignUp.js +153 -0
- package/src/client/components/core/SocketIo.js +163 -0
- package/src/client/components/core/SocketIoHandler.js +75 -0
- package/src/client/components/core/Stream.js +161 -0
- package/src/client/components/core/ToggleSwitch.js +89 -0
- package/src/client/components/core/ToolTip.js +78 -0
- package/src/client/components/core/Translate.js +694 -0
- package/src/client/components/core/Validator.js +140 -0
- package/src/client/components/core/VanillaJs.js +485 -0
- package/src/client/components/core/Wallet.js +95 -0
- package/src/client/components/core/WebComponent.js +44 -0
- package/src/client/components/core/Webhook.js +58 -0
- package/src/client/components/core/Worker.js +313 -0
- package/src/client/components/core/windowGetDimensions.js +269 -0
- package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +352 -0
- package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
- package/src/client/components/cryptokoyn/CssCryptokoyn.js +212 -0
- package/src/client/components/cryptokoyn/LogInCryptokoyn.js +16 -0
- package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +12 -0
- package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
- package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +16 -0
- package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +11 -0
- package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +6 -0
- package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
- package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
- package/src/client/components/cyberia/InstanceEngineCyberia.js +1088 -0
- package/src/client/components/cyberia/InstanceSelectionView.js +900 -0
- package/src/client/components/cyberia/MapEngineCyberia.js +1976 -0
- package/src/client/components/cyberia/ObjectLayerEngine.js +1540 -0
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +2420 -0
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +1522 -0
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +528 -0
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +772 -0
- package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +254 -0
- package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +21 -0
- package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +16 -0
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +518 -0
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +72 -0
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +16 -0
- package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +13 -0
- package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +6 -0
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +48 -0
- package/src/client/components/default/AppShellDefault.js +765 -0
- package/src/client/components/default/AppStoreDefault.js +5 -0
- package/src/client/components/default/CommonDefault.js +29 -0
- package/src/client/components/default/CssDefault.js +27 -0
- package/src/client/components/default/LogInDefault.js +16 -0
- package/src/client/components/default/LogOutDefault.js +12 -0
- package/src/client/components/default/RouterDefault.js +47 -0
- package/src/client/components/default/SettingsDefault.js +16 -0
- package/src/client/components/default/SignUpDefault.js +11 -0
- package/src/client/components/default/SocketIoDefault.js +6 -0
- package/src/client/components/default/TranslateDefault.js +7 -0
- package/src/client/components/itemledger/AppShellItemledger.js +353 -0
- package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
- package/src/client/components/itemledger/CssItemledger.js +212 -0
- package/src/client/components/itemledger/LogInItemledger.js +17 -0
- package/src/client/components/itemledger/LogOutItemledger.js +12 -0
- package/src/client/components/itemledger/RouterItemledger.js +38 -0
- package/src/client/components/itemledger/SettingsItemledger.js +16 -0
- package/src/client/components/itemledger/SignUpItemledger.js +11 -0
- package/src/client/components/itemledger/SocketIoItemledger.js +6 -0
- package/src/client/components/itemledger/TranslateItemledger.js +7 -0
- package/src/client/components/underpost/AppShellUnderpost.js +691 -0
- package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
- package/src/client/components/underpost/CssUnderpost.js +281 -0
- package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +879 -0
- package/src/client/components/underpost/DocumentSearchProvider.js +448 -0
- package/src/client/components/underpost/LabGalleryUnderpost.js +79 -0
- package/src/client/components/underpost/LogInUnderpost.js +25 -0
- package/src/client/components/underpost/LogOutUnderpost.js +17 -0
- package/src/client/components/underpost/RouterUnderpost.js +45 -0
- package/src/client/components/underpost/SettingsUnderpost.js +16 -0
- package/src/client/components/underpost/SignUpUnderpost.js +11 -0
- package/src/client/components/underpost/SocketIoUnderpost.js +6 -0
- package/src/client/components/underpost/TranslateUnderpost.js +10 -0
- package/src/client/public/cryptokoyn/assets/banner/koyn-social.png +0 -0
- package/src/client/public/cryptokoyn/assets/logo/base-icon.png +0 -0
- package/src/client/public/cryptokoyn/assets/logo/cryptokoyn.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/android-chrome-144x144.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/android-chrome-192x192.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/android-chrome-256x256.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/android-chrome-36x36.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/android-chrome-48x48.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/android-chrome-72x72.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/android-chrome-96x96.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-114x114-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-114x114.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-120x120-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-120x120.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-144x144-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-144x144.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-152x152-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-152x152.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-180x180-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-180x180.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-57x57-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-57x57.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-60x60-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-60x60.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-72x72-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-72x72.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-76x76-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-76x76.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon-precomposed.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/apple-touch-icon.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/favicon-16x16.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/favicon-32x32.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/mstile-144x144.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/mstile-150x150.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/mstile-310x150.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/mstile-310x310.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/mstile-70x70.png +0 -0
- package/src/client/public/cryptokoyn/assets/splash/safari-pinned-tab.svg +42 -0
- package/src/client/public/cryptokoyn/browserconfig.xml +12 -0
- package/src/client/public/cryptokoyn/favicon.ico +0 -0
- package/src/client/public/cryptokoyn/microdata.json +85 -0
- package/src/client/public/cryptokoyn/site.webmanifest +57 -0
- package/src/client/public/cryptokoyn/sitemap +148 -0
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +307 -0
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +309 -0
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +204 -0
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +341 -0
- package/src/client/public/cyberia-docs/CYBERIA-LORE.md +84 -0
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +294 -0
- package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +242 -0
- package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
- package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +309 -0
- package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +228 -0
- package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
- package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +740 -0
- package/src/client/public/default/android-chrome-144x144.png +0 -0
- package/src/client/public/default/android-chrome-192x192.png +0 -0
- package/src/client/public/default/android-chrome-256x256.png +0 -0
- package/src/client/public/default/android-chrome-36x36.png +0 -0
- package/src/client/public/default/android-chrome-384x384.png +0 -0
- package/src/client/public/default/android-chrome-48x48.png +0 -0
- package/src/client/public/default/android-chrome-512x512.png +0 -0
- package/src/client/public/default/android-chrome-72x72.png +0 -0
- package/src/client/public/default/android-chrome-96x96.png +0 -0
- package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
- package/src/client/public/default/apple-touch-icon-114x114-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
- package/src/client/public/default/apple-touch-icon-120x120-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
- package/src/client/public/default/apple-touch-icon-144x144-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
- package/src/client/public/default/apple-touch-icon-152x152-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
- package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
- package/src/client/public/default/apple-touch-icon-180x180-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
- package/src/client/public/default/apple-touch-icon-57x57-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
- package/src/client/public/default/apple-touch-icon-60x60-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
- package/src/client/public/default/apple-touch-icon-72x72-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
- package/src/client/public/default/apple-touch-icon-76x76-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
- package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
- package/src/client/public/default/assets/background/dark.jpg +0 -0
- package/src/client/public/default/assets/background/dark.svg +557 -0
- package/src/client/public/default/assets/background/white.jpg +0 -0
- package/src/client/public/default/assets/background/white0-min.jpg +0 -0
- package/src/client/public/default/assets/background/white0.jpg +0 -0
- package/src/client/public/default/assets/logo/base-icon.png +0 -0
- package/src/client/public/default/assets/logo/underpost.gif +0 -0
- package/src/client/public/default/assets/mailer/api-user-check.png +0 -0
- package/src/client/public/default/assets/mailer/api-user-default-avatar.png +0 -0
- package/src/client/public/default/assets/mailer/api-user-invalid-token.png +0 -0
- package/src/client/public/default/assets/mailer/api-user-recover.png +0 -0
- package/src/client/public/default/browserconfig.xml +12 -0
- package/src/client/public/default/favicon-16x16.png +0 -0
- package/src/client/public/default/favicon-32x32.png +0 -0
- package/src/client/public/default/favicon-48x48.png +0 -0
- package/src/client/public/default/favicon.ico +0 -0
- package/src/client/public/default/manifest.webmanifest +69 -0
- package/src/client/public/default/mstile-144x144.png +0 -0
- package/src/client/public/default/mstile-150x150.png +0 -0
- package/src/client/public/default/mstile-310x150.png +0 -0
- package/src/client/public/default/mstile-310x310.png +0 -0
- package/src/client/public/default/mstile-70x70.png +0 -0
- package/src/client/public/default/safari-pinned-tab.svg +24 -0
- package/src/client/public/default/site.webmanifest +69 -0
- package/src/client/public/default/sitemap +148 -0
- package/src/client/public/default/yandex-browser-50x50.png +0 -0
- package/src/client/public/default/yandex-browser-manifest.json +9 -0
- package/src/client/public/doc/favicon.ico +0 -0
- package/src/client/public/doc/sitemap +148 -0
- package/src/client/public/itemledger/android-chrome-144x144.png +0 -0
- package/src/client/public/itemledger/android-chrome-192x192.png +0 -0
- package/src/client/public/itemledger/android-chrome-256x256.png +0 -0
- package/src/client/public/itemledger/android-chrome-36x36.png +0 -0
- package/src/client/public/itemledger/android-chrome-384x384.png +0 -0
- package/src/client/public/itemledger/android-chrome-48x48.png +0 -0
- package/src/client/public/itemledger/android-chrome-512x512.png +0 -0
- package/src/client/public/itemledger/android-chrome-72x72.png +0 -0
- package/src/client/public/itemledger/android-chrome-96x96.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-1024x1024.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-114x114.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-120x120.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-144x144.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-152x152.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-167x167.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-180x180.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-57x57.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-60x60.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-72x72.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-76x76.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon-precomposed.png +0 -0
- package/src/client/public/itemledger/apple-touch-icon.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1125x2436.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1136x640.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1170x2532.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1179x2556.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1242x2208.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1242x2688.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1284x2778.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1290x2796.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1334x750.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1488x2266.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1536x2048.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1620x2160.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1640x2160.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1668x2224.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1668x2388.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-1792x828.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2048x1536.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2048x2732.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2160x1620.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2160x1640.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2208x1242.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2224x1668.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2266x1488.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2388x1668.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2436x1125.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2532x1170.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2556x1179.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2688x1242.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2732x2048.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2778x1284.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-2796x1290.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-640x1136.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-750x1334.png +0 -0
- package/src/client/public/itemledger/apple-touch-startup-image-828x1792.png +0 -0
- package/src/client/public/itemledger/assets/logo/base-icon.png +0 -0
- package/src/client/public/itemledger/browserconfig.xml +12 -0
- package/src/client/public/itemledger/favicon-16x16.png +0 -0
- package/src/client/public/itemledger/favicon-32x32.png +0 -0
- package/src/client/public/itemledger/favicon-48x48.png +0 -0
- package/src/client/public/itemledger/favicon.ico +0 -0
- package/src/client/public/itemledger/manifest.webmanifest +69 -0
- package/src/client/public/itemledger/microdata.json +71 -0
- package/src/client/public/itemledger/mstile-144x144.png +0 -0
- package/src/client/public/itemledger/mstile-150x150.png +0 -0
- package/src/client/public/itemledger/mstile-310x150.png +0 -0
- package/src/client/public/itemledger/mstile-310x310.png +0 -0
- package/src/client/public/itemledger/mstile-70x70.png +0 -0
- package/src/client/public/itemledger/sitemap +148 -0
- package/src/client/public/itemledger/yandex-browser-50x50.png +0 -0
- package/src/client/public/itemledger/yandex-browser-manifest.json +9 -0
- package/src/client/public/test/favicon.ico +0 -0
- package/src/client/public/test/sitemap +148 -0
- package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +137 -0
- package/src/client/services/core/core.service.js +309 -0
- package/src/client/services/crypto/crypto.service.js +68 -0
- package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
- package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
- package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +99 -0
- package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
- package/src/client/services/cyberia-entity/cyberia-entity.service.js +99 -0
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +137 -0
- package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +99 -0
- package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
- package/src/client/services/cyberia-map/cyberia-map.service.js +120 -0
- package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
- package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
- package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
- package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
- package/src/client/services/default/default.management.js +1015 -0
- package/src/client/services/default/default.service.js +99 -0
- package/src/client/services/document/document.service.js +134 -0
- package/src/client/services/file/file.service.js +94 -0
- package/src/client/services/instance/instance.management.js +78 -0
- package/src/client/services/instance/instance.service.js +107 -0
- package/src/client/services/ipfs/ipfs.service.js +118 -0
- package/src/client/services/object-layer/object-layer.management.js +414 -0
- package/src/client/services/object-layer/object-layer.service.js +208 -0
- package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +99 -0
- package/src/client/services/test/test.service.js +68 -0
- package/src/client/services/user/guest.service.js +86 -0
- package/src/client/services/user/user.management.js +51 -0
- package/src/client/services/user/user.service.js +106 -0
- package/src/client/ssr/RootDocument.js +237 -0
- package/src/client/ssr/body/404.js +77 -0
- package/src/client/ssr/body/500.js +76 -0
- package/src/client/ssr/body/CacheControl.js +117 -0
- package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +97 -0
- package/src/client/ssr/body/DefaultSplashScreen.js +97 -0
- package/src/client/ssr/body/SwaggerDarkMode.js +285 -0
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +90 -0
- package/src/client/ssr/head/CryptokoynScripts.js +4 -0
- package/src/client/ssr/head/Css.js +241 -0
- package/src/client/ssr/head/CyberiaPortalScripts.js +6 -0
- package/src/client/ssr/head/DefaultScripts.js +6 -0
- package/src/client/ssr/head/ItemledgerScripts.js +4 -0
- package/src/client/ssr/head/Microdata.js +11 -0
- package/src/client/ssr/head/Production.js +1 -0
- package/src/client/ssr/head/Pwa.js +146 -0
- package/src/client/ssr/head/PwaItemledger.js +197 -0
- package/src/client/ssr/head/Seo.js +15 -0
- package/src/client/ssr/head/UnderpostScripts.js +6 -0
- package/src/client/ssr/mailer/DefaultRecoverEmail.js +20 -0
- package/src/client/ssr/mailer/DefaultVerifyEmail.js +16 -0
- package/src/client/ssr/views/Cyberia404.js +328 -0
- package/src/client/ssr/views/CyberiaServerMetrics.js +1070 -0
- package/src/client/ssr/views/Maintenance.js +64 -0
- package/src/client/ssr/views/NoNetworkConnection.js +68 -0
- package/src/client/ssr/views/Test.js +199 -0
- package/src/client/sw/core.sw.js +274 -0
- package/src/client-builder/client-build-docs.js +493 -0
- package/src/client-builder/client-build-live.js +100 -0
- package/src/client-builder/client-build.js +1092 -0
- package/src/client-builder/client-dev-server.js +88 -0
- package/src/client-builder/client-formatted.js +170 -0
- package/src/client-builder/client-icons.js +108 -0
- package/src/client-builder/ssr.js +124 -0
- package/src/client.build.js +17 -0
- package/src/client.dev.js +21 -0
- package/src/db/DataBaseProvider.js +213 -0
- package/src/db/mariadb/MariaDB.js +72 -0
- package/src/db/mongo/MongoBootstrap.js +657 -0
- package/src/db/mongo/MongooseDB.js +199 -0
- package/src/grpc/cyberia/grpc-server.js +162 -0
- package/src/index.js +357 -0
- package/src/mailer/EmailRender.js +116 -0
- package/src/mailer/MailerProvider.js +213 -0
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +413 -0
- package/src/projects/cyberia/besu-genesis-generator.js +1630 -0
- package/src/projects/cyberia/catalog-cyberia.js +90 -0
- package/src/projects/cyberia/gemini-client.js +175 -0
- package/src/projects/cyberia/generate-saga.js +2137 -0
- package/src/projects/cyberia/hot-reload-trigger.js +163 -0
- package/src/projects/cyberia/instance-data.js +719 -0
- package/src/projects/cyberia/ipfs-client.js +599 -0
- package/src/projects/cyberia/map-preview-generator.js +304 -0
- package/src/projects/cyberia/object-layer.js +978 -0
- package/src/projects/cyberia/semantic-layer-generator-floor.js +319 -0
- package/src/projects/cyberia/semantic-layer-generator-resource.js +259 -0
- package/src/projects/cyberia/semantic-layer-generator-skin.js +1164 -0
- package/src/projects/cyberia/semantic-layer-generator.js +753 -0
- package/src/projects/cyberia/shape-generator.js +1060 -0
- package/src/projects/underpost/catalog-underpost.js +60 -0
- package/src/proxy.js +19 -0
- package/src/runtime/cyberia-client/Dockerfile +49 -0
- package/src/runtime/cyberia-client/Dockerfile.dev +75 -0
- package/src/runtime/cyberia-server/Dockerfile +43 -0
- package/src/runtime/cyberia-server/Dockerfile.dev +56 -0
- package/src/runtime/engine-cyberia/Dockerfile +161 -0
- package/src/runtime/engine-cyberia/Dockerfile.dev +161 -0
- package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
- package/src/runtime/engine-cyberia/compose.env +144 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +434 -0
- package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
- package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
- package/src/runtime/engine-cyberia/nginx.conf +128 -0
- package/src/runtime/express/Dockerfile +45 -0
- package/src/runtime/express/Express.js +285 -0
- package/src/runtime/lampp/Dockerfile +62 -0
- package/src/runtime/lampp/Lampp.js +359 -0
- package/src/runtime/nginx/Nginx.js +250 -0
- package/src/runtime/wp/Dockerfile +62 -0
- package/src/runtime/wp/Wp.js +642 -0
- package/src/server/auth.js +713 -0
- package/src/server/backup.js +102 -0
- package/src/server/catalog.js +72 -0
- package/src/server/conf.js +2316 -0
- package/src/server/cron.js +476 -0
- package/src/server/crypto.js +210 -0
- package/src/server/data-query.js +461 -0
- package/src/server/dns.js +553 -0
- package/src/server/downloader.js +73 -0
- package/src/server/logger.js +229 -0
- package/src/server/middlewares.js +152 -0
- package/src/server/peer.js +75 -0
- package/src/server/process.js +299 -0
- package/src/server/proxy.js +139 -0
- package/src/server/runtime-status.js +252 -0
- package/src/server/runtime.js +183 -0
- package/src/server/start.js +272 -0
- package/src/server/tls.js +254 -0
- package/src/server/valkey.js +201 -0
- package/src/server.js +25 -0
- package/src/ws/IoInterface.js +129 -0
- package/src/ws/IoServer.js +68 -0
- package/src/ws/core/channels/core.ws.chat.js +68 -0
- package/src/ws/core/channels/core.ws.mailer.js +116 -0
- package/src/ws/core/channels/core.ws.stream.js +90 -0
- package/src/ws/core/core.ws.connection.js +41 -0
- package/src/ws/core/core.ws.emit.js +37 -0
- package/src/ws/core/core.ws.server.js +44 -0
- package/src/ws/default/channels/default.ws.main.js +57 -0
- package/src/ws/default/default.ws.connection.js +35 -0
- package/src/ws/default/default.ws.emit.js +14 -0
- package/src/ws/default/default.ws.server.js +39 -0
- package/test/api.test.js +53 -0
- package/test/crypto.test.js +117 -0
- package/test/cyberia-instance-conf-defaults.test.js +140 -0
- package/test/deploy-monitor.test.js +267 -0
- package/test/shape-generator.test.js +463 -0
- package/tsconfig.docs.json +15 -0
- package/typedoc.dd-cyberia.json +31 -0
- package/typedoc.json +31 -0
|
@@ -0,0 +1,3947 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides baremetal provisioning and configuration functionalities.
|
|
3
|
+
* @module src/cli/baremetal.js
|
|
4
|
+
* @namespace UnderpostBaremetal
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
|
|
9
|
+
import { pbcopy, shellExec } from '../server/process.js';
|
|
10
|
+
import dotenv from 'dotenv';
|
|
11
|
+
import { loggerFactory, loggerMiddleware } from '../server/logger.js';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import Downloader from '../server/downloader.js';
|
|
15
|
+
import { newInstance, range, s4, timer } from '../client/components/core/CommonJs.js';
|
|
16
|
+
import { spawnSync } from 'child_process';
|
|
17
|
+
import Underpost from '../index.js';
|
|
18
|
+
import express from 'express';
|
|
19
|
+
|
|
20
|
+
const logger = loggerFactory(import.meta);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @class UnderpostBaremetal
|
|
24
|
+
* @description Manages baremetal provisioning and configuration tasks.
|
|
25
|
+
* This class provides a set of static methods to automate various
|
|
26
|
+
* infrastructure operations, including NFS management, control server setup,
|
|
27
|
+
* and system provisioning for different architectures.
|
|
28
|
+
*/
|
|
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
|
+
|
|
37
|
+
// Lifecycle events POSTed by the ephemeral runtime to the bootstrap HTTP
|
|
38
|
+
// server, keyed by hostname. Populated by httpBootstrapServerRunnerFactory's
|
|
39
|
+
// POST handler and consumed by waitForBootstrapStage during orchestration.
|
|
40
|
+
static bootstrapStatusEvents = new Map();
|
|
41
|
+
|
|
42
|
+
static API = {
|
|
43
|
+
/**
|
|
44
|
+
* @method callback
|
|
45
|
+
* @description Initiates a baremetal provisioning workflow based on the provided options.
|
|
46
|
+
* This is the primary entry point for orchestrating baremetal operations.
|
|
47
|
+
* It handles NFS root filesystem building, control server installation/uninstallation,
|
|
48
|
+
* and system-level provisioning tasks like timezone and keyboard configuration.
|
|
49
|
+
* @param {string} [workflowId='rpi4mb'] - Identifier for the specific workflow configuration to use.
|
|
50
|
+
* @param {object} [options] - An object containing boolean flags for various operations.
|
|
51
|
+
* @param {string} [options.ipAddress=getLocalIPv4Address()] - The IP address of the control server or the local machine.
|
|
52
|
+
* @param {string} [options.hostname=workflowId] - The hostname of the target baremetal machine.
|
|
53
|
+
* @param {string} [options.ipFileServer=getLocalIPv4Address()] - The IP address of the file server (NFS/TFTP).
|
|
54
|
+
* @param {string} [options.ipConfig=''] - IP configuration string for the baremetal machine.
|
|
55
|
+
* @param {string} [options.netmask=''] - Netmask of network
|
|
56
|
+
* @param {string} [options.dnsServer=''] - DNS server IP address.
|
|
57
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
58
|
+
* @param {boolean} [options.controlServerInstall=false] - Flag to install the control server (e.g., MAAS).
|
|
59
|
+
* @param {boolean} [options.controlServerUninstall=false] - Flag to uninstall the control server.
|
|
60
|
+
* @param {boolean} [options.controlServerRestart=false] - Flag to restart the control server.
|
|
61
|
+
* @param {boolean} [options.controlServerDbInstall=false] - Flag to install the control server's database.
|
|
62
|
+
* @param {boolean} [options.createMachine=false] - Flag to create a machine in MAAS.
|
|
63
|
+
* @param {boolean} [options.controlServerDbUninstall=false] - Flag to uninstall the control server's database.
|
|
64
|
+
* @param {string} [options.mac=''] - MAC address of the baremetal machine.
|
|
65
|
+
* @param {boolean} [options.ipxe=false] - Flag to use iPXE for booting.
|
|
66
|
+
* @param {boolean} [options.ipxeRebuild=false] - Flag to rebuild the iPXE binary with embedded script.
|
|
67
|
+
* @param {string} [options.ipxeBuildIso=''] - Builds a standalone iPXE ISO with embedded script for the specified workflow ID.
|
|
68
|
+
* @param {boolean} [options.installPacker=false] - Flag to install Packer CLI.
|
|
69
|
+
* @param {string} [options.packerMaasImageTemplate] - Template path from canonical/packer-maas to extract (requires workflow-id).
|
|
70
|
+
* @param {string} [options.packerWorkflowId] - Workflow ID for Packer MAAS image operations (used with --packer-maas-image-build or --packer-maas-image-upload).
|
|
71
|
+
* @param {boolean} [options.packerMaasImageBuild=false] - Flag to build a Packer MAAS image for the workflow specified by packerWorkflowId.
|
|
72
|
+
* @param {boolean} [options.packerMaasImageUpload=false] - Flag to upload a Packer MAAS image artifact without rebuilding for the workflow specified by packerWorkflowId.
|
|
73
|
+
* @param {boolean} [options.packerMaasImageCached=false] - Flag to use cached artifacts when building the Packer MAAS image.
|
|
74
|
+
* @param {string} [options.removeMachines=''] - Comma-separated list of machine system IDs or '*' to remove existing machines from MAAS before commissioning.
|
|
75
|
+
* @param {boolean} [options.clearDiscovered=false] - Flag to clear discovered machines from MAAS before commissioning.
|
|
76
|
+
* @param {boolean} [options.cloudInitUpdate=false] - Flag to update cloud-init configuration on the baremetal machine.
|
|
77
|
+
* @param {boolean} [options.commission=false] - Flag to commission the baremetal machine.
|
|
78
|
+
* @param {number} [options.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server.
|
|
79
|
+
* @param {string} [options.bootstrapHttpServerPath='./public/localhost'] - Path for the bootstrap HTTP server files.
|
|
80
|
+
* @param {boolean} [options.bootstrapHttpServerRun=false] - Flag to start the bootstrap HTTP server.
|
|
81
|
+
* @param {string} [options.isoUrl=''] - Uses a custom ISO URL for baremetal machine commissioning.
|
|
82
|
+
* @param {boolean} [options.ubuntuToolsBuild=false] - Builds ubuntu tools for chroot environment.
|
|
83
|
+
* @param {boolean} [options.ubuntuToolsTest=false] - Tests ubuntu tools in chroot environment.
|
|
84
|
+
* @param {boolean} [options.rockyToolsBuild=false] - Builds rocky linux tools for chroot environment.
|
|
85
|
+
* @param {boolean} [options.rockyToolsTest=false] - Tests rocky linux tools in chroot environment.
|
|
86
|
+
* @param {string} [options.bootcmd=''] - Comma-separated list of boot commands to execute.
|
|
87
|
+
* @param {string} [options.runcmd=''] - Comma-separated list of run commands to execute.
|
|
88
|
+
* @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
|
|
89
|
+
* @param {boolean} [options.nfsBuildServer=false] - Flag to build the NFS server components.
|
|
90
|
+
* @param {boolean} [options.nfsMount=false] - Flag to mount the NFS root filesystem.
|
|
91
|
+
* @param {boolean} [options.nfsReset=false] - Flag to reset the NFS environment by unmounting and cleaning the host path.
|
|
92
|
+
* @param {boolean} [options.nfsUnmount=false] - Flag to unmount the NFS root filesystem.
|
|
93
|
+
* @param {boolean} [options.nfsSh=false] - Flag to chroot into the NFS environment for shell access.
|
|
94
|
+
* @param {string} [options.logs=''] - Specifies which logs to display ('dhcp', 'cloud', 'machine', 'cloud-config').
|
|
95
|
+
* @param {string} [options.installDisk=''] - Specifies the disk to install the OS on (e.g., /dev/sda).
|
|
96
|
+
* @param {boolean} [options.autoInstall=true] - Flag to enable automatic installation of the OS on the baremetal machine.
|
|
97
|
+
* @param {boolean} [options.remoteInstall=true] - Flag to enable remote installation of the OS on the baremetal machine.
|
|
98
|
+
* @param {boolean} [options.worker=false] - Flag to designate the machine as a worker node.
|
|
99
|
+
* @param {string} [options.control=''] - Specifies the control node for the baremetal machine.
|
|
100
|
+
* @param {string} [options.sshKeyDir=''] - Specifies the directory containing SSH keys for the baremetal machine.
|
|
101
|
+
* @param {string} [options.deployId=''] - Specifies the deployment ID for SSH key resolution.
|
|
102
|
+
* @param {string} [options.engineRepo=''] - Specifies the custom engine repository URL.
|
|
103
|
+
* @param {string} [options.engineBranch=''] - Specifies the custom engine repository branch.
|
|
104
|
+
* @param {string} [options.enginePrivateRepo=''] - Specifies the custom private engine repository URL.
|
|
105
|
+
* @param {string} [options.enginePrivateBranch=''] - Specifies the custom private engine repository branch.
|
|
106
|
+
* @param {string} [options.user=''] - Specifies the SSH user for the baremetal machine.
|
|
107
|
+
* @param {boolean} [options.resumeInfraSetup=false] - Flag to skip commissioning and OS install, resuming SSH-based infra setup on an already installed node.
|
|
108
|
+
* @param {boolean} [options.resumeJoin=false] - Flag to skip everything except the kubeadm join command.
|
|
109
|
+
* @memberof UnderpostBaremetal
|
|
110
|
+
* @returns {void}
|
|
111
|
+
*/
|
|
112
|
+
async callback(
|
|
113
|
+
workflowId,
|
|
114
|
+
options = {
|
|
115
|
+
ipAddress: undefined,
|
|
116
|
+
hostname: undefined,
|
|
117
|
+
ipFileServer: undefined,
|
|
118
|
+
ipConfig: undefined,
|
|
119
|
+
netmask: undefined,
|
|
120
|
+
dnsServer: undefined,
|
|
121
|
+
dev: false,
|
|
122
|
+
controlServerInstall: false,
|
|
123
|
+
controlServerUninstall: false,
|
|
124
|
+
controlServerRestart: false,
|
|
125
|
+
controlServerDbInstall: false,
|
|
126
|
+
controlServerDbUninstall: false,
|
|
127
|
+
createMachine: false,
|
|
128
|
+
mac: '',
|
|
129
|
+
ipxe: false,
|
|
130
|
+
ipxeRebuild: false,
|
|
131
|
+
ipxeBuildIso: '',
|
|
132
|
+
installPacker: false,
|
|
133
|
+
packerMaasImageTemplate: false,
|
|
134
|
+
packerWorkflowId: '',
|
|
135
|
+
packerMaasImageBuild: false,
|
|
136
|
+
packerMaasImageUpload: false,
|
|
137
|
+
packerMaasImageCached: false,
|
|
138
|
+
removeMachines: '',
|
|
139
|
+
clearDiscovered: false,
|
|
140
|
+
cloudInitUpdate: false,
|
|
141
|
+
cloudInit: false,
|
|
142
|
+
commission: false,
|
|
143
|
+
bootstrapHttpServerPort: 8888,
|
|
144
|
+
bootstrapHttpServerPath: './public/localhost',
|
|
145
|
+
bootstrapHttpServerRun: false,
|
|
146
|
+
isoUrl: '',
|
|
147
|
+
ubuntuToolsBuild: false,
|
|
148
|
+
ubuntuToolsTest: false,
|
|
149
|
+
rockyToolsBuild: false,
|
|
150
|
+
rockyToolsTest: false,
|
|
151
|
+
bootcmd: '',
|
|
152
|
+
runcmd: '',
|
|
153
|
+
nfsBuild: false,
|
|
154
|
+
nfsBuildServer: false,
|
|
155
|
+
nfsMount: false,
|
|
156
|
+
nfsReset: false,
|
|
157
|
+
nfsUnmount: false,
|
|
158
|
+
nfsSh: false,
|
|
159
|
+
logs: '',
|
|
160
|
+
installDisk: '',
|
|
161
|
+
autoInstall: true,
|
|
162
|
+
remoteInstall: true,
|
|
163
|
+
worker: false,
|
|
164
|
+
control: '',
|
|
165
|
+
sshKeyDir: '',
|
|
166
|
+
user: '',
|
|
167
|
+
deployId: '',
|
|
168
|
+
engineRepo: '',
|
|
169
|
+
engineBranch: '',
|
|
170
|
+
enginePrivateRepo: '',
|
|
171
|
+
enginePrivateBranch: '',
|
|
172
|
+
resumeInfraSetup: false,
|
|
173
|
+
resumeJoin: false,
|
|
174
|
+
},
|
|
175
|
+
) {
|
|
176
|
+
let { ipAddress, hostname, ipFileServer, ipConfig, netmask, dnsServer } = options;
|
|
177
|
+
|
|
178
|
+
// Determine the root path for npm and underpost.
|
|
179
|
+
const npmRoot = getNpmRootPath();
|
|
180
|
+
const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
|
|
181
|
+
|
|
182
|
+
// Set default values if not provided.
|
|
183
|
+
workflowId = workflowId ? workflowId : 'rpi4mbarm64-iso-ram';
|
|
184
|
+
hostname = hostname ? hostname : workflowId;
|
|
185
|
+
ipAddress = ipAddress ? ipAddress : '192.168.1.191';
|
|
186
|
+
ipFileServer = ipFileServer ? ipFileServer : Underpost.dns.getLocalIPv4Address();
|
|
187
|
+
netmask = netmask ? netmask : '255.255.255.0';
|
|
188
|
+
dnsServer = dnsServer ? dnsServer : '8.8.8.8';
|
|
189
|
+
|
|
190
|
+
// IpConfig options:
|
|
191
|
+
// dhcp - DHCP configuration
|
|
192
|
+
// dhpc6 - DHCP IPv6 configuration
|
|
193
|
+
// auto6 - automatic IPv6 configuration
|
|
194
|
+
// on, any - any protocol available in the kernel (default)
|
|
195
|
+
// none, off - no autoconfiguration, static network configuration
|
|
196
|
+
ipConfig = ipConfig ? ipConfig : 'none';
|
|
197
|
+
|
|
198
|
+
// Set default MAC address
|
|
199
|
+
let macAddress = Underpost.baremetal.macAddressFactory(options).mac;
|
|
200
|
+
const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
|
|
201
|
+
|
|
202
|
+
if (!workflowsConfig[workflowId]) {
|
|
203
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const tftpPrefix = workflowsConfig[workflowId].tftpPrefix || 'rpi4mb';
|
|
207
|
+
// Define the bootstrap architecture.
|
|
208
|
+
let bootstrapArch;
|
|
209
|
+
|
|
210
|
+
// Set bootstrap architecture.
|
|
211
|
+
if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
|
|
212
|
+
const { architecture } = workflowsConfig[workflowId].debootstrap.image;
|
|
213
|
+
bootstrapArch = architecture;
|
|
214
|
+
} else if (workflowsConfig[workflowId].type === 'chroot-container') {
|
|
215
|
+
const { architecture } = workflowsConfig[workflowId].container;
|
|
216
|
+
bootstrapArch = architecture;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Define the database provider ID.
|
|
220
|
+
const dbProviderId = 'postgresql-17';
|
|
221
|
+
|
|
222
|
+
// Define the NFS host path based on the environment variable and hostname.
|
|
223
|
+
const nfsHostPath = `${process.env.NFS_EXPORT_PATH}/${hostname}`;
|
|
224
|
+
|
|
225
|
+
// Define the TFTP root prefix path based
|
|
226
|
+
const tftpRootPath = `${process.env.TFTP_ROOT}/${tftpPrefix}`;
|
|
227
|
+
|
|
228
|
+
// Define the iPXE cache directory to preserve builds across tftproot cleanups
|
|
229
|
+
const ipxeCacheDir = `/tmp/ipxe-cache/${tftpPrefix}`;
|
|
230
|
+
|
|
231
|
+
// Define the bootstrap HTTP server path.
|
|
232
|
+
const bootstrapHttpServerPath = options.bootstrapHttpServerPath
|
|
233
|
+
? options.bootstrapHttpServerPath
|
|
234
|
+
: `/tmp/bootstrap-http-server/${workflowId}`;
|
|
235
|
+
|
|
236
|
+
// Capture metadata for the callback execution, useful for logging and auditing.
|
|
237
|
+
const callbackMetaData = {
|
|
238
|
+
args: { workflowId, ipAddress, hostname, ipFileServer, ipConfig, netmask, dnsServer },
|
|
239
|
+
options,
|
|
240
|
+
runnerHost: { architecture: Underpost.baremetal.getHostArch().alias, ip: Underpost.dns.getLocalIPv4Address() },
|
|
241
|
+
nfsHostPath,
|
|
242
|
+
tftpRootPath,
|
|
243
|
+
bootstrapHttpServerPath,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Log the initiation of the baremetal callback with relevant metadata.
|
|
247
|
+
logger.info('Baremetal callback', callbackMetaData);
|
|
248
|
+
|
|
249
|
+
// --resume-infra-setup: skip commissioning, OS install, and all PXE/TFTP/MAAS
|
|
250
|
+
// bootstrapping; directly resume the SSH-based infra setup on a node that
|
|
251
|
+
// already has the OS installed and is reachable via SSH.
|
|
252
|
+
if (options.resumeInfraSetup) {
|
|
253
|
+
const { privateKeyPath, user: resolvedUser } = Underpost.baremetal.resolveSshKeyPaths({
|
|
254
|
+
options,
|
|
255
|
+
workflowsConfig,
|
|
256
|
+
workflowId,
|
|
257
|
+
});
|
|
258
|
+
logger.info('--resume-infra-setup: skipping commission/bootstrapping; resuming SSH infra setup', {
|
|
259
|
+
hostname,
|
|
260
|
+
ipAddress,
|
|
261
|
+
keyPath: privateKeyPath,
|
|
262
|
+
user: resolvedUser,
|
|
263
|
+
workflowId,
|
|
264
|
+
infraSetup: workflowsConfig[workflowId]?.infraSetup || 'none',
|
|
265
|
+
});
|
|
266
|
+
return await Underpost.baremetal.postInstallDispatcher({
|
|
267
|
+
workflowId,
|
|
268
|
+
workflowsConfig,
|
|
269
|
+
hostname,
|
|
270
|
+
ipAddress,
|
|
271
|
+
options,
|
|
272
|
+
underpostRoot,
|
|
273
|
+
keyPath: privateKeyPath,
|
|
274
|
+
controlUser: resolvedUser,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --resume-join: skip everything except the kubeadm join command.
|
|
279
|
+
// Even lighter than --resume-infra-setup: no engine setup, no npm install,
|
|
280
|
+
// no init-host, no config. Assumes all infra is already installed.
|
|
281
|
+
if (options.resumeJoin) {
|
|
282
|
+
const { privateKeyPath, user: resolvedUser } = Underpost.baremetal.resolveSshKeyPaths({
|
|
283
|
+
options,
|
|
284
|
+
workflowsConfig,
|
|
285
|
+
workflowId,
|
|
286
|
+
});
|
|
287
|
+
logger.info('--resume-join: skipping all bootstrapping; joining cluster directly with minimal SSH command', {
|
|
288
|
+
hostname,
|
|
289
|
+
ipAddress,
|
|
290
|
+
keyPath: privateKeyPath,
|
|
291
|
+
user: resolvedUser,
|
|
292
|
+
});
|
|
293
|
+
return await Underpost.baremetal.infraSetupKubeadm({
|
|
294
|
+
hostname,
|
|
295
|
+
ipAddress,
|
|
296
|
+
options,
|
|
297
|
+
underpostRoot,
|
|
298
|
+
keyPath: privateKeyPath,
|
|
299
|
+
controlUser: resolvedUser,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Create a new machine in MAAS if the option is set.
|
|
304
|
+
let machine;
|
|
305
|
+
if (options.createMachine === true) {
|
|
306
|
+
const [searhMachine] = Underpost.baremetal.maasCliExec(`machines read hostname=${hostname}`);
|
|
307
|
+
|
|
308
|
+
if (searhMachine) {
|
|
309
|
+
// Check if existing machine's MAC matches the specified MAC
|
|
310
|
+
const existingMac = searhMachine.boot_interface?.mac_address || searhMachine.mac_address;
|
|
311
|
+
|
|
312
|
+
// If using hardware MAC (macAddress is null), skip MAC validation and use existing machine
|
|
313
|
+
if (macAddress === null) {
|
|
314
|
+
logger.info(`Using hardware MAC mode - keeping existing machine ${hostname} with MAC ${existingMac}`);
|
|
315
|
+
machine = searhMachine;
|
|
316
|
+
} else if (existingMac && existingMac !== macAddress) {
|
|
317
|
+
logger.warn(`⚠ Machine ${hostname} exists with MAC ${existingMac}, but --mac specified ${macAddress}`);
|
|
318
|
+
logger.info(`Deleting existing machine ${searhMachine.system_id} to recreate with correct MAC...`);
|
|
319
|
+
|
|
320
|
+
// Delete the existing machine
|
|
321
|
+
Underpost.baremetal.maasCliExec(`machine delete ${searhMachine.system_id}`);
|
|
322
|
+
|
|
323
|
+
// Create new machine with correct MAC
|
|
324
|
+
machine = Underpost.baremetal.machineFactory({
|
|
325
|
+
hostname,
|
|
326
|
+
ipAddress,
|
|
327
|
+
macAddress,
|
|
328
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
329
|
+
}).machine;
|
|
330
|
+
|
|
331
|
+
logger.info(`✓ Machine recreated with MAC ${macAddress}`);
|
|
332
|
+
} else {
|
|
333
|
+
logger.info(`Using existing machine ${hostname} with MAC ${existingMac}`);
|
|
334
|
+
machine = searhMachine;
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
// No existing machine found, create new one
|
|
338
|
+
// For hardware MAC mode (macAddress is null), we'll create machine after discovery
|
|
339
|
+
if (macAddress === null) {
|
|
340
|
+
logger.info(`Hardware MAC mode - machine will be created after discovery`);
|
|
341
|
+
machine = null;
|
|
342
|
+
} else {
|
|
343
|
+
machine = Underpost.baremetal.machineFactory({
|
|
344
|
+
hostname,
|
|
345
|
+
ipAddress,
|
|
346
|
+
macAddress,
|
|
347
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
348
|
+
}).machine;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (options.ipxeBuildIso)
|
|
354
|
+
return await Underpost.baremetal.ipxeBuildIso({
|
|
355
|
+
workflowId,
|
|
356
|
+
isoOutputPath: options.ipxeBuildIso,
|
|
357
|
+
tftpPrefix,
|
|
358
|
+
ipFileServer,
|
|
359
|
+
ipAddress,
|
|
360
|
+
ipConfig,
|
|
361
|
+
netmask,
|
|
362
|
+
dnsServer,
|
|
363
|
+
macAddress,
|
|
364
|
+
cloudInit: options.cloudInit,
|
|
365
|
+
dev: options.dev,
|
|
366
|
+
forceRebuild: options.ipxeRebuild,
|
|
367
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
368
|
+
port: options.bootstrapHttpServerPort,
|
|
369
|
+
workflowId,
|
|
370
|
+
workflowsConfig,
|
|
371
|
+
}),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (options.installPacker) {
|
|
375
|
+
await Underpost.baremetal.installPacker(underpostRoot);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (options.packerMaasImageTemplate) {
|
|
380
|
+
workflowId = options.packerWorkflowId;
|
|
381
|
+
if (!workflowId) {
|
|
382
|
+
throw new Error('--packer-workflow-id is required when using --packer-maas-image-template');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const templatePath = options.packerMaasImageTemplate;
|
|
386
|
+
const targetDir = `${underpostRoot}/packer/images/${workflowId}`;
|
|
387
|
+
|
|
388
|
+
logger.info(`Creating new Packer MAAS image template for workflow: ${workflowId}`);
|
|
389
|
+
logger.info(`Template path: ${templatePath}`);
|
|
390
|
+
logger.info(`Target directory: ${targetDir}`);
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
// Use Underpost.repo to copy files from GitHub
|
|
394
|
+
const result = await Underpost.repo.copyGitUrlDirectoryRecursive({
|
|
395
|
+
gitUrl: 'https://github.com/canonical/packer-maas',
|
|
396
|
+
directoryPath: templatePath,
|
|
397
|
+
targetPath: targetDir,
|
|
398
|
+
branch: 'main',
|
|
399
|
+
overwrite: false,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
logger.info(`\nSuccessfully copied ${result.filesCount} files`);
|
|
403
|
+
|
|
404
|
+
// Create empty workflow configuration entry
|
|
405
|
+
const workflowConfig = {
|
|
406
|
+
dir: `packer/images/${workflowId}`,
|
|
407
|
+
maas: {
|
|
408
|
+
name: `custom/${workflowId.toLowerCase()}`,
|
|
409
|
+
title: `${workflowId} Custom`,
|
|
410
|
+
architecture: 'amd64/generic',
|
|
411
|
+
base_image: 'ubuntu/22.04',
|
|
412
|
+
filetype: 'tgz',
|
|
413
|
+
content: `${workflowId.toLowerCase()}.tar.gz`,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const workflows = Underpost.baremetal.loadPackerMaasImageBuildWorkflows();
|
|
418
|
+
workflows[workflowId] = workflowConfig;
|
|
419
|
+
Underpost.baremetal.writePackerMaasImageBuildWorkflows(workflows);
|
|
420
|
+
|
|
421
|
+
logger.info('Template extracted successfully!');
|
|
422
|
+
logger.info(`Added configuration for ${workflowId} to engine/baremetal/packer-workflows.json`);
|
|
423
|
+
logger.info('Next steps');
|
|
424
|
+
logger.info(`1. Review and customize the Packer template files in: ${targetDir}`);
|
|
425
|
+
logger.info(`2. Review the workflow configuration in engine/baremetal/packer-workflows.json`);
|
|
426
|
+
logger.info(
|
|
427
|
+
`3. Build the image with: underpost baremetal --packer-workflow-id ${workflowId} --packer-maas-image-build`,
|
|
428
|
+
);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
throw new Error(`Failed to extract template: ${error.message}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (options.packerMaasImageBuild || options.packerMaasImageUpload) {
|
|
437
|
+
// Use the workflow ID from --packer-workflow-id option
|
|
438
|
+
if (!options.packerWorkflowId) {
|
|
439
|
+
throw new Error('Workflow ID is required. Please specify using --packer-workflow-id <workflow-id>');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
workflowId = options.packerWorkflowId;
|
|
443
|
+
|
|
444
|
+
const workflow = Underpost.baremetal.loadPackerMaasImageBuildWorkflows()[workflowId];
|
|
445
|
+
if (!workflow) {
|
|
446
|
+
throw new Error(`Packer MAAS image build workflow not found: ${workflowId}`);
|
|
447
|
+
}
|
|
448
|
+
const packerDir = `${underpostRoot}/${workflow.dir}`;
|
|
449
|
+
const tarballPath = `${packerDir}/${workflow.maas.content}`;
|
|
450
|
+
|
|
451
|
+
// Build phase (skip if upload-only mode)
|
|
452
|
+
if (options.packerMaasImageBuild) {
|
|
453
|
+
if (shellExec('packer version', { silentOnError: true }).code !== 0) {
|
|
454
|
+
throw new Error('Packer is not installed. Please install Packer to proceed.');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Check for QEMU support if building for a different architecture (validator bots case)
|
|
458
|
+
Underpost.baremetal.checkQemuCrossArchSupport(workflow);
|
|
459
|
+
|
|
460
|
+
logger.info(`Building Packer image for ${workflowId} in ${packerDir}...`);
|
|
461
|
+
|
|
462
|
+
// Only remove artifacts if not using cached mode
|
|
463
|
+
if (!options.packerMaasImageCached) {
|
|
464
|
+
const artifacts = [
|
|
465
|
+
'output-rocky9',
|
|
466
|
+
'packer_cache',
|
|
467
|
+
'x86_64_VARS.fd',
|
|
468
|
+
'aarch64_VARS.fd',
|
|
469
|
+
workflow.maas.content,
|
|
470
|
+
];
|
|
471
|
+
shellExec(`cd packer/images/${workflowId}
|
|
472
|
+
rm -rf ${artifacts.join(' ')}`);
|
|
473
|
+
logger.info('Removed previous build artifacts');
|
|
474
|
+
} else {
|
|
475
|
+
logger.info('Cached mode: Keeping existing artifacts for incremental build');
|
|
476
|
+
}
|
|
477
|
+
shellExec(`chmod +x ${underpostRoot}/scripts/packer-init-vars-file.sh`);
|
|
478
|
+
shellExec(`${underpostRoot}/scripts/packer-init-vars-file.sh`);
|
|
479
|
+
|
|
480
|
+
const init = spawnSync('packer', ['init', '.'], { stdio: 'inherit', cwd: packerDir });
|
|
481
|
+
if (init.status !== 0) {
|
|
482
|
+
throw new Error('Packer init failed');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const isArm = process.arch === 'arm64';
|
|
486
|
+
// Add /usr/local/bin to PATH so Packer can find compiled QEMU binaries
|
|
487
|
+
const packerEnv = {
|
|
488
|
+
...process.env,
|
|
489
|
+
PACKER_LOG: '1',
|
|
490
|
+
PATH: `/usr/local/bin:${process.env.PATH || '/usr/bin:/bin'}`,
|
|
491
|
+
};
|
|
492
|
+
const build = spawnSync('packer', ['build', '-var', `host_is_arm=${isArm}`, '.'], {
|
|
493
|
+
stdio: 'inherit',
|
|
494
|
+
cwd: packerDir,
|
|
495
|
+
env: packerEnv,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (build.status !== 0) {
|
|
499
|
+
throw new Error('Packer build failed');
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
// Upload-only mode: verify tarball exists
|
|
503
|
+
logger.info(`Upload-only mode: checking for existing build artifact...`);
|
|
504
|
+
if (!fs.existsSync(tarballPath)) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Build artifact not found: ${tarballPath}\n` +
|
|
507
|
+
`Please build first with: --packer-workflow-id ${workflowId} --packer-maas-image-build`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const stats = fs.statSync(tarballPath);
|
|
511
|
+
logger.info(`Found existing artifact: ${tarballPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
logger.info(`Uploading image to MAAS...`);
|
|
515
|
+
|
|
516
|
+
// Use the upload script to avoid MAAS CLI bugs
|
|
517
|
+
const uploadScript = `${underpostRoot}/scripts/maas-upload-boot-resource.sh`;
|
|
518
|
+
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}"`;
|
|
519
|
+
|
|
520
|
+
logger.info(`Uploading to MAAS using: ${uploadScript}`);
|
|
521
|
+
// silentOnError: caller logs stdout/stderr structure on failure
|
|
522
|
+
// before throwing its own, more informative error.
|
|
523
|
+
const uploadResult = shellExec(uploadCmd, { silentOnError: true });
|
|
524
|
+
if (uploadResult.code !== 0) {
|
|
525
|
+
logger.error(`Upload failed with exit code: ${uploadResult.code}`);
|
|
526
|
+
if (uploadResult.stdout) {
|
|
527
|
+
logger.error(`Upload output:\n${uploadResult.stdout}`);
|
|
528
|
+
}
|
|
529
|
+
if (uploadResult.stderr) {
|
|
530
|
+
logger.error(`Upload error output:\n${uploadResult.stderr}`);
|
|
531
|
+
}
|
|
532
|
+
throw new Error('MAAS upload failed - see output above for details');
|
|
533
|
+
}
|
|
534
|
+
logger.info(`Successfully uploaded ${workflow.maas.name} to MAAS!`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Handle various log display options.
|
|
539
|
+
if (options.logs === 'dhcp') {
|
|
540
|
+
shellExec(`journalctl -f -t dhcpd -u snap.maas.pebble.service`);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (options.logs === 'dhcp-lease') {
|
|
545
|
+
shellExec(`cat /var/snap/maas/common/maas/dhcp/dhcpd.leases`);
|
|
546
|
+
shellExec(`cat /var/snap/maas/common/maas/dhcp/dhcpd.pid`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (options.logs === 'dhcp-lan') {
|
|
551
|
+
shellExec(`sudo tcpdump -l -n -i any -s0 -vv 'udp and (port 67 or 68)'`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (options.logs === 'cloud-init') {
|
|
556
|
+
shellExec(`tail -f -n 900 ${nfsHostPath}/var/log/cloud-init.log`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (options.logs === 'cloud-init-machine') {
|
|
561
|
+
shellExec(`tail -f -n 900 ${nfsHostPath}/var/log/cloud-init-output.log`);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (options.logs === 'cloud-init-config') {
|
|
566
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`);
|
|
567
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/meta-data`);
|
|
568
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Handle NFS shell access option.
|
|
573
|
+
if (options.nfsSh === true) {
|
|
574
|
+
// Copy the chroot command to the clipboard for easy execution.
|
|
575
|
+
if (bootstrapArch && bootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
576
|
+
switch (bootstrapArch) {
|
|
577
|
+
case 'arm64':
|
|
578
|
+
pbcopy(`sudo chroot ${nfsHostPath} /usr/bin/qemu-aarch64-static /bin/bash`);
|
|
579
|
+
break;
|
|
580
|
+
|
|
581
|
+
case 'amd64':
|
|
582
|
+
pbcopy(`sudo chroot ${nfsHostPath} /usr/bin/qemu-x86_64-static /bin/bash`);
|
|
583
|
+
break;
|
|
584
|
+
|
|
585
|
+
default:
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
else pbcopy(`sudo chroot ${nfsHostPath} /bin/bash`);
|
|
589
|
+
|
|
590
|
+
return; // Exit early as this is a specific interactive operation.
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Handle control server installation.
|
|
594
|
+
if (options.controlServerInstall === true) {
|
|
595
|
+
// Ensure the MAAS setup script is executable and then run it.
|
|
596
|
+
shellExec(`chmod +x ${underpostRoot}/scripts/maas-setup.sh`);
|
|
597
|
+
if (!fs.existsSync(`${process.env.HOME}/.ssh/id_rsa.pub`)) shellExec(`node bin ssh --generate`);
|
|
598
|
+
shellExec(`${underpostRoot}/scripts/maas-setup.sh`);
|
|
599
|
+
// Install GRUB modules into the NFS root filesystem to
|
|
600
|
+
// ensure the necessary files are present for bootloader installation later.
|
|
601
|
+
Underpost.baremetal.installGrubModules();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Handle control server uninstallation.
|
|
606
|
+
if (options.controlServerUninstall === true) {
|
|
607
|
+
// Stop and remove MAAS services, handling potential errors gracefully.
|
|
608
|
+
shellExec(`sudo snap stop maas.pebble`);
|
|
609
|
+
shellExec(`sudo snap stop maas`);
|
|
610
|
+
shellExec(`sudo snap remove maas --purge`);
|
|
611
|
+
|
|
612
|
+
// Remove residual snap data to ensure a clean uninstall.
|
|
613
|
+
shellExec(`sudo rm -rf /var/snap/maas`);
|
|
614
|
+
shellExec(`sudo rm -rf ~/snap/maas`);
|
|
615
|
+
|
|
616
|
+
// Remove MAAS configuration and data directories.
|
|
617
|
+
shellExec(`sudo rm -rf /etc/maas`);
|
|
618
|
+
shellExec(`sudo rm -rf /var/lib/maas`);
|
|
619
|
+
shellExec(`sudo rm -rf /var/log/maas`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Handle control server restart.
|
|
624
|
+
if (options.controlServerRestart === true) {
|
|
625
|
+
shellExec(`sudo snap restart maas`);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Handle control server database installation.
|
|
630
|
+
if (options.controlServerDbInstall === true) {
|
|
631
|
+
// Deploy the database provider and manage MAAS database.
|
|
632
|
+
shellExec(`node ${underpostRoot}/bin/deploy ${dbProviderId} install`);
|
|
633
|
+
shellExec(`node ${underpostRoot}/bin/deploy maas-db`);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Handle control server database uninstallation.
|
|
638
|
+
if (options.controlServerDbUninstall === true) {
|
|
639
|
+
shellExec(`node ${underpostRoot}/bin/deploy ${dbProviderId} uninstall`);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Handle NFS mount operation.
|
|
644
|
+
if (options.nfsMount === true) {
|
|
645
|
+
await Underpost.baremetal.nfsMountCallback({
|
|
646
|
+
hostname,
|
|
647
|
+
nfsHostPath,
|
|
648
|
+
workflowId,
|
|
649
|
+
mount: true,
|
|
650
|
+
});
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Handle NFS unmount operation.
|
|
655
|
+
if (options.nfsUnmount === true) {
|
|
656
|
+
await Underpost.baremetal.nfsMountCallback({
|
|
657
|
+
hostname,
|
|
658
|
+
nfsHostPath,
|
|
659
|
+
workflowId,
|
|
660
|
+
unmount: true,
|
|
661
|
+
});
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Handle NFS root filesystem build operation.
|
|
666
|
+
if (options.nfsBuild === true) {
|
|
667
|
+
await Underpost.baremetal.nfsMountCallback({
|
|
668
|
+
hostname,
|
|
669
|
+
nfsHostPath,
|
|
670
|
+
workflowId,
|
|
671
|
+
unmount: true,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Clean and create the NFS host path.
|
|
675
|
+
shellExec(`sudo rm -rf ${nfsHostPath}/*`);
|
|
676
|
+
shellExec(`mkdir -p ${nfsHostPath}`);
|
|
677
|
+
|
|
678
|
+
// Perform the first stage of debootstrap.
|
|
679
|
+
if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
|
|
680
|
+
const { architecture, name } = workflowsConfig[workflowId].debootstrap.image;
|
|
681
|
+
shellExec(
|
|
682
|
+
[
|
|
683
|
+
`sudo debootstrap`,
|
|
684
|
+
`--arch=${architecture}`,
|
|
685
|
+
`--variant=minbase`,
|
|
686
|
+
`--foreign`, // Indicates a two-stage debootstrap.
|
|
687
|
+
name,
|
|
688
|
+
nfsHostPath,
|
|
689
|
+
`http://ports.ubuntu.com/ubuntu-ports/`,
|
|
690
|
+
].join(' '),
|
|
691
|
+
);
|
|
692
|
+
} else if (workflowsConfig[workflowId].type === 'chroot-container') {
|
|
693
|
+
const { image } = workflowsConfig[workflowId].container;
|
|
694
|
+
shellExec(`sudo podman pull --arch=${bootstrapArch} ${image}`);
|
|
695
|
+
shellExec(`sudo podman create --arch=${bootstrapArch} --name chroot-source ${image}`);
|
|
696
|
+
shellExec(`sudo podman export chroot-source | sudo tar -x -C ${nfsHostPath}`);
|
|
697
|
+
shellExec(`sudo podman rm chroot-source`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Create a podman container to extract QEMU static binaries.
|
|
701
|
+
shellExec(`sudo podman create --name extract docker.io/multiarch/qemu-user-static`);
|
|
702
|
+
shellExec(`podman ps -a`); // List all podman containers for verification.
|
|
703
|
+
|
|
704
|
+
// If cross-architecture, copy the QEMU static binary into the chroot.
|
|
705
|
+
if (bootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
706
|
+
Underpost.baremetal.crossArchBinFactory({
|
|
707
|
+
nfsHostPath,
|
|
708
|
+
bootstrapArch,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Clean up the temporary podman container.
|
|
712
|
+
shellExec(`sudo podman rm extract`);
|
|
713
|
+
shellExec(`podman ps -a`);
|
|
714
|
+
shellExec(`file ${nfsHostPath}/bin/bash`); // Verify the bash executable in the chroot.
|
|
715
|
+
|
|
716
|
+
// Mount necessary filesystems and register binfmt for the second stage.
|
|
717
|
+
await Underpost.baremetal.nfsMountCallback({
|
|
718
|
+
hostname,
|
|
719
|
+
nfsHostPath,
|
|
720
|
+
workflowId,
|
|
721
|
+
mount: true,
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Perform the second stage of debootstrap within the chroot environment.
|
|
725
|
+
if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
|
|
726
|
+
Underpost.baremetal.crossArchRunner({
|
|
727
|
+
nfsHostPath,
|
|
728
|
+
bootstrapArch,
|
|
729
|
+
callbackMetaData,
|
|
730
|
+
steps: [`/debootstrap/debootstrap --second-stage`],
|
|
731
|
+
});
|
|
732
|
+
} else if (
|
|
733
|
+
workflowsConfig[workflowId].type === 'chroot-container' &&
|
|
734
|
+
workflowsConfig[workflowId].osIdLike.match('rhel')
|
|
735
|
+
) {
|
|
736
|
+
// Copy resolv.conf to allow network access inside chroot
|
|
737
|
+
shellExec(`sudo cp /etc/resolv.conf ${nfsHostPath}/etc/resolv.conf`);
|
|
738
|
+
|
|
739
|
+
// Consolidate all package installations into one step to avoid redundancy
|
|
740
|
+
const { packages } = workflowsConfig[workflowId].container;
|
|
741
|
+
const basePackages = [
|
|
742
|
+
'findutils',
|
|
743
|
+
'systemd',
|
|
744
|
+
'sudo',
|
|
745
|
+
'dracut',
|
|
746
|
+
'dracut-network',
|
|
747
|
+
'dracut-config-generic',
|
|
748
|
+
'nfs-utils',
|
|
749
|
+
'file',
|
|
750
|
+
'binutils',
|
|
751
|
+
'kernel-modules-core',
|
|
752
|
+
'NetworkManager',
|
|
753
|
+
'dhclient',
|
|
754
|
+
'iputils',
|
|
755
|
+
];
|
|
756
|
+
const allPackages = packages && packages.length > 0 ? [...basePackages, ...packages] : basePackages;
|
|
757
|
+
|
|
758
|
+
Underpost.baremetal.crossArchRunner({
|
|
759
|
+
nfsHostPath,
|
|
760
|
+
bootstrapArch,
|
|
761
|
+
callbackMetaData,
|
|
762
|
+
steps: [
|
|
763
|
+
`dnf install -y --allowerasing ${allPackages.join(
|
|
764
|
+
' ',
|
|
765
|
+
)} 2>/dev/null || yum install -y --allowerasing ${allPackages.join(
|
|
766
|
+
' ',
|
|
767
|
+
)} 2>/dev/null || echo "Package install completed"`,
|
|
768
|
+
`dnf clean all`,
|
|
769
|
+
`echo "=== Installed packages verification ==="`,
|
|
770
|
+
`rpm -qa | grep -E "dracut|kernel|nfs" | sort`,
|
|
771
|
+
`echo "=== Boot directory contents ==="`,
|
|
772
|
+
`ls -la /boot /lib/modules/*/`,
|
|
773
|
+
// Search for bootable kernel in order of preference:
|
|
774
|
+
// 1. Raw ARM64 Image file (preferred for GRUB)
|
|
775
|
+
// 2. vmlinuz or vmlinux (may be PE32+ on Rocky Linux)
|
|
776
|
+
`echo "Searching for bootable kernel..."`,
|
|
777
|
+
`KERNEL_FILE=""`,
|
|
778
|
+
// First try to find raw Image file
|
|
779
|
+
`if [ -f /boot/Image ]; then KERNEL_FILE=/boot/Image; echo "Found raw ARM64 Image: $KERNEL_FILE"; fi`,
|
|
780
|
+
`if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "Image" -o -name "Image.gz" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found kernel Image in modules: $KERNEL_FILE"; fi`,
|
|
781
|
+
// Fallback to vmlinuz
|
|
782
|
+
`if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /boot -name "vmlinuz-*" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinuz: $KERNEL_FILE"; fi`,
|
|
783
|
+
`if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "vmlinuz" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinuz in modules: $KERNEL_FILE"; fi`,
|
|
784
|
+
// Last resort: any vmlinux
|
|
785
|
+
`if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "vmlinux" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinux: $KERNEL_FILE"; fi`,
|
|
786
|
+
`if [ -z "$KERNEL_FILE" ]; then echo "ERROR: No kernel found!"; exit 1; fi`,
|
|
787
|
+
// Copy and check kernel type
|
|
788
|
+
`cp "$KERNEL_FILE" /boot/vmlinuz-efi.tmp`,
|
|
789
|
+
// Decompress if gzipped
|
|
790
|
+
`if file /boot/vmlinuz-efi.tmp | grep -q gzip; then echo "Decompressing gzipped kernel..."; gunzip -c /boot/vmlinuz-efi.tmp > /boot/vmlinuz-efi && rm /boot/vmlinuz-efi.tmp; else mv /boot/vmlinuz-efi.tmp /boot/vmlinuz-efi; fi`,
|
|
791
|
+
`KERNEL_TYPE=$(file /boot/vmlinuz-efi 2>/dev/null)`,
|
|
792
|
+
`echo "Final kernel file type: $KERNEL_TYPE"`,
|
|
793
|
+
// Handle PE32+ if still present - use kernel directly without extraction since iPXE can boot it
|
|
794
|
+
`case "$KERNEL_TYPE" in *PE32+*|*EFI*application*) echo "WARNING: Kernel is PE32+ EFI executable"; echo "GRUB may fail to boot this - recommend using iPXE chainload or installing kernel-core package"; echo "Keeping PE32+ kernel as-is for now..."; ;; *ARM64*|*aarch64*|*Image*|*data*) echo "Kernel appears to be raw ARM64 format - suitable for GRUB"; ;; *) echo "Unknown kernel format - attempting to use anyway"; ;; esac`,
|
|
795
|
+
// Get kernel version for initramfs rebuild
|
|
796
|
+
`KVER=$(basename $(dirname "$KERNEL_FILE"))`,
|
|
797
|
+
`echo "Kernel version: $KVER"`,
|
|
798
|
+
// Rebuild initramfs with NFS and network support
|
|
799
|
+
`echo "Rebuilding initramfs with NFS and network support..."`,
|
|
800
|
+
`echo "Available dracut modules:"`,
|
|
801
|
+
`dracut --list-modules 2>/dev/null | grep -E "network|nfs" || echo "No network modules listed"`,
|
|
802
|
+
// Use network-manager module (it's available in Rocky 9) for better compatibility
|
|
803
|
+
`dracut --force --add "nfs network base" --add-drivers "nfs sunrpc" --kver "$KVER" /boot/initrd.img "$KVER" 2>&1 || echo "Initramfs rebuild failed"`,
|
|
804
|
+
// Fallback: if rebuild fails, use existing initramfs
|
|
805
|
+
`if [ ! -f /boot/initrd.img ]; then echo "Initramfs rebuild failed, using existing..."; INITRD=$(find /boot -name "initramfs-$KVER.img" 2>/dev/null | head -n 1); if [ -z "$INITRD" ]; then INITRD=$(find /boot -name "initramfs*.img" 2>/dev/null | grep -v kdump | head -n 1); fi; if [ -n "$INITRD" ]; then cp "$INITRD" /boot/initrd.img; echo "Copied existing initramfs: $INITRD"; else echo "ERROR: No initramfs found!"; fi; fi`,
|
|
806
|
+
`echo "=== Final boot files ==="`,
|
|
807
|
+
`ls -lh /boot/vmlinuz-efi /boot/initrd.img`,
|
|
808
|
+
`file /boot/vmlinuz-efi`,
|
|
809
|
+
`file /boot/initrd.img`,
|
|
810
|
+
`echo "=== Setting root password ==="`,
|
|
811
|
+
`echo "root:root" | chpasswd`,
|
|
812
|
+
],
|
|
813
|
+
});
|
|
814
|
+
} else {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`Unsupported workflow type for NFS build: ${workflowsConfig[workflowId].type} and like os ID ${workflowsConfig[workflowId].osIdLike}`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Fetch boot resources and machines if commissioning or listing.
|
|
822
|
+
|
|
823
|
+
let resources = Underpost.baremetal.maasCliExec(`boot-resources read`).map((o) => ({
|
|
824
|
+
id: o.id,
|
|
825
|
+
name: o.name,
|
|
826
|
+
architecture: o.architecture,
|
|
827
|
+
}));
|
|
828
|
+
if (options.ls === true) {
|
|
829
|
+
console.table(resources);
|
|
830
|
+
}
|
|
831
|
+
let machines = Underpost.baremetal.maasCliExec(`machines read`).map((m) => ({
|
|
832
|
+
system_id: m.interface_set[0].system_id,
|
|
833
|
+
mac_address: m.interface_set[0].mac_address,
|
|
834
|
+
hostname: m.hostname,
|
|
835
|
+
status_name: m.status_name,
|
|
836
|
+
}));
|
|
837
|
+
if (options.ls === true) {
|
|
838
|
+
console.table(machines);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (options.clearDiscovered) Underpost.baremetal.removeDiscoveredMachines();
|
|
842
|
+
|
|
843
|
+
// Handle remove existing machines from MAAS.
|
|
844
|
+
if (options.removeMachines)
|
|
845
|
+
machines = Underpost.baremetal.removeMachines({
|
|
846
|
+
machines: options.removeMachines === 'all' ? machines : options.removeMachines.split(','),
|
|
847
|
+
ignore: machine ? [machine.system_id] : [],
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
|
|
851
|
+
if (options.ubuntuToolsBuild) {
|
|
852
|
+
Underpost.cloudInit.buildTools({
|
|
853
|
+
workflowId,
|
|
854
|
+
nfsHostPath,
|
|
855
|
+
hostname,
|
|
856
|
+
callbackMetaData,
|
|
857
|
+
dev: options.dev,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const { chronyc, keyboard } = workflowsConfig[workflowId];
|
|
861
|
+
const { timezone, chronyConfPath } = chronyc;
|
|
862
|
+
const systemProvisioning = 'ubuntu';
|
|
863
|
+
|
|
864
|
+
Underpost.baremetal.crossArchRunner({
|
|
865
|
+
nfsHostPath,
|
|
866
|
+
bootstrapArch,
|
|
867
|
+
callbackMetaData,
|
|
868
|
+
steps: [
|
|
869
|
+
...Underpost.system.factory[systemProvisioning].base(),
|
|
870
|
+
...Underpost.system.factory[systemProvisioning].user(),
|
|
871
|
+
...Underpost.system.factory[systemProvisioning].timezone({
|
|
872
|
+
timezone,
|
|
873
|
+
chronyConfPath,
|
|
874
|
+
}),
|
|
875
|
+
...Underpost.system.factory[systemProvisioning].keyboard(keyboard.layout),
|
|
876
|
+
],
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (options.ubuntuToolsTest)
|
|
881
|
+
Underpost.baremetal.crossArchRunner({
|
|
882
|
+
nfsHostPath,
|
|
883
|
+
bootstrapArch,
|
|
884
|
+
callbackMetaData,
|
|
885
|
+
steps: [
|
|
886
|
+
`chmod +x /underpost/date.sh`,
|
|
887
|
+
`chmod +x /underpost/keyboard.sh`,
|
|
888
|
+
`chmod +x /underpost/dns.sh`,
|
|
889
|
+
`chmod +x /underpost/help.sh`,
|
|
890
|
+
`chmod +x /underpost/host.sh`,
|
|
891
|
+
`chmod +x /underpost/test.sh`,
|
|
892
|
+
`chmod +x /underpost/start.sh`,
|
|
893
|
+
`chmod +x /underpost/reset.sh`,
|
|
894
|
+
`chmod +x /underpost/shutdown.sh`,
|
|
895
|
+
`chmod +x /underpost/device_scan.sh`,
|
|
896
|
+
`chmod +x /underpost/mac.sh`,
|
|
897
|
+
`sudo chmod 700 ~/.ssh/`, // Set secure permissions for .ssh directory.
|
|
898
|
+
`sudo chmod 600 ~/.ssh/authorized_keys`, // Set secure permissions for authorized_keys.
|
|
899
|
+
`sudo chmod 644 ~/.ssh/known_hosts`, // Set permissions for known_hosts.
|
|
900
|
+
`sudo chmod 600 ~/.ssh/id_rsa`, // Set secure permissions for private key.
|
|
901
|
+
`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`, // Set secure permissions for host key.
|
|
902
|
+
`chown -R root:root ~/.ssh`, // Ensure root owns the .ssh directory.
|
|
903
|
+
`/underpost/test.sh`,
|
|
904
|
+
],
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (
|
|
909
|
+
workflowsConfig[workflowId].type === 'chroot-container' &&
|
|
910
|
+
workflowsConfig[workflowId].osIdLike.match('rhel')
|
|
911
|
+
) {
|
|
912
|
+
if (options.rockyToolsBuild) {
|
|
913
|
+
const { chronyc, keyboard } = workflowsConfig[workflowId];
|
|
914
|
+
const { timezone } = chronyc;
|
|
915
|
+
const systemProvisioning = 'rocky';
|
|
916
|
+
|
|
917
|
+
Underpost.baremetal.crossArchRunner({
|
|
918
|
+
nfsHostPath,
|
|
919
|
+
bootstrapArch,
|
|
920
|
+
callbackMetaData,
|
|
921
|
+
steps: [
|
|
922
|
+
...Underpost.system.factory[systemProvisioning].base(),
|
|
923
|
+
...Underpost.system.factory[systemProvisioning].user(),
|
|
924
|
+
...Underpost.system.factory[systemProvisioning].timezone({
|
|
925
|
+
timezone,
|
|
926
|
+
chronyConfPath: chronyc.chronyConfPath,
|
|
927
|
+
}),
|
|
928
|
+
...Underpost.system.factory[systemProvisioning].keyboard(keyboard.layout),
|
|
929
|
+
],
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (options.rockyToolsTest)
|
|
934
|
+
Underpost.baremetal.crossArchRunner({
|
|
935
|
+
nfsHostPath,
|
|
936
|
+
bootstrapArch,
|
|
937
|
+
callbackMetaData,
|
|
938
|
+
steps: [
|
|
939
|
+
`node --version`,
|
|
940
|
+
`npm --version`,
|
|
941
|
+
`underpost --version`,
|
|
942
|
+
`timedatectl status`,
|
|
943
|
+
`localectl status`,
|
|
944
|
+
`id root`,
|
|
945
|
+
`ls -la /home/root/.ssh/`,
|
|
946
|
+
`cat /home/root/.ssh/authorized_keys`,
|
|
947
|
+
'underpost test',
|
|
948
|
+
],
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Generate MAAS authentication credentials
|
|
953
|
+
const authCredentials =
|
|
954
|
+
options.commission || options.cloudInit || options.cloudInitUpdate
|
|
955
|
+
? Underpost.baremetal.maasAuthCredentialsFactory()
|
|
956
|
+
: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' };
|
|
957
|
+
|
|
958
|
+
// Generate cloud-init configuration if needed for commissioning or cloud-init update workflows.
|
|
959
|
+
let cloudConfigSrc = '';
|
|
960
|
+
if (options.cloudInit || options.cloudInitUpdate) {
|
|
961
|
+
const { chronyc, networkInterfaceName } = workflowsConfig[workflowId];
|
|
962
|
+
const { timezone, chronyConfPath } = chronyc;
|
|
963
|
+
|
|
964
|
+
let write_files = [];
|
|
965
|
+
let runcmd = options.runcmd;
|
|
966
|
+
|
|
967
|
+
if (machine && options.commission) {
|
|
968
|
+
write_files = Underpost.baremetal.commissioningWriteFilesFactory({
|
|
969
|
+
machine,
|
|
970
|
+
authCredentials,
|
|
971
|
+
runnerHostIp: callbackMetaData.runnerHost.ip,
|
|
972
|
+
});
|
|
973
|
+
runcmd = '/usr/local/bin/underpost-enlist.sh';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
cloudConfigSrc = Underpost.cloudInit.configFactory(
|
|
977
|
+
{
|
|
978
|
+
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
979
|
+
hostname,
|
|
980
|
+
commissioningDeviceIp: ipAddress,
|
|
981
|
+
gatewayip: callbackMetaData.runnerHost.ip,
|
|
982
|
+
mac: macAddress,
|
|
983
|
+
timezone,
|
|
984
|
+
chronyConfPath,
|
|
985
|
+
networkInterfaceName,
|
|
986
|
+
ubuntuToolsBuild: options.ubuntuToolsBuild,
|
|
987
|
+
bootcmd: options.bootcmd,
|
|
988
|
+
runcmd,
|
|
989
|
+
write_files,
|
|
990
|
+
},
|
|
991
|
+
authCredentials,
|
|
992
|
+
).cloudConfigSrc;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Rocky/RHEL Kickstart generation
|
|
996
|
+
let kickstartSrc = '';
|
|
997
|
+
if (Underpost.baremetal.getFamilyBaseOs(workflowsConfig[workflowId].osIdLike).isRhelBased) {
|
|
998
|
+
const bootstrapPort = Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
999
|
+
port: options.bootstrapHttpServerPort,
|
|
1000
|
+
workflowId,
|
|
1001
|
+
workflowsConfig,
|
|
1002
|
+
});
|
|
1003
|
+
// Base URL the ephemeral runtime POSTs lifecycle events to:
|
|
1004
|
+
// http://<controller-ip>:<port>/<hostname> -> .../status
|
|
1005
|
+
const bootstrapUrl = `http://${callbackMetaData.runnerHost.ip}:${bootstrapPort}/${hostname}`;
|
|
1006
|
+
const { publicKeyPath } = Underpost.baremetal.resolveSshKeyPaths({ options, workflowsConfig, workflowId });
|
|
1007
|
+
if (!fs.existsSync(publicKeyPath)) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`SSH public key not found at ${publicKeyPath}. Set --ssh-key-dir <dir> (expects <dir>/id_rsa.pub) or the workflow "sshKeyDir".`,
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
const { rootPassword, adminUsername, adminPassword, deployUsername, deployPassword } =
|
|
1013
|
+
Underpost.baremetal.resolveInstallCredentials({ options, workflowsConfig, workflowId });
|
|
1014
|
+
const { netIp, netPrefix, netGateway, netDns } = Underpost.baremetal.resolveInstalledNetwork({
|
|
1015
|
+
ipAddress,
|
|
1016
|
+
netmask,
|
|
1017
|
+
dnsServer,
|
|
1018
|
+
});
|
|
1019
|
+
logger.info('Resolved installed-OS login + network', {
|
|
1020
|
+
adminUsername,
|
|
1021
|
+
adminPasswordSet: Boolean(adminPassword),
|
|
1022
|
+
deployUsername: deployUsername || '(none)',
|
|
1023
|
+
rootPasswordSet: Boolean(rootPassword),
|
|
1024
|
+
staticIp: `${netIp}/${netPrefix}`,
|
|
1025
|
+
gateway: netGateway,
|
|
1026
|
+
});
|
|
1027
|
+
kickstartSrc = Underpost.kickstart.kickstartFactory({
|
|
1028
|
+
lang: 'en_US.UTF-8',
|
|
1029
|
+
keyboard: workflowsConfig[workflowId].keyboard?.layout,
|
|
1030
|
+
timezone: workflowsConfig[workflowId].chronyc?.timezone,
|
|
1031
|
+
chronyConfPath: workflowsConfig[workflowId].chronyc?.chronyConfPath,
|
|
1032
|
+
rootPassword,
|
|
1033
|
+
adminUsername,
|
|
1034
|
+
adminPassword,
|
|
1035
|
+
deployUsername,
|
|
1036
|
+
deployPassword,
|
|
1037
|
+
netIp,
|
|
1038
|
+
netPrefix,
|
|
1039
|
+
netGateway,
|
|
1040
|
+
netDns,
|
|
1041
|
+
authorizedKeys: fs.readFileSync(publicKeyPath, 'utf8').trim(),
|
|
1042
|
+
bootstrapUrl,
|
|
1043
|
+
workflowId,
|
|
1044
|
+
systemId: machine?.system_id || '',
|
|
1045
|
+
targetHostname: hostname,
|
|
1046
|
+
sshPort: 22,
|
|
1047
|
+
installDiskHint:
|
|
1048
|
+
(typeof options.installDisk === 'string' ? options.installDisk : '') ||
|
|
1049
|
+
workflowsConfig[workflowId].installDisk ||
|
|
1050
|
+
'',
|
|
1051
|
+
autoInstall: options.autoInstall !== false,
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Build and optionally run the HTTP bootstrap server to serve cloud-init, kickstart, and ISO resources for commissioning and provisioning.
|
|
1056
|
+
if (cloudConfigSrc || kickstartSrc || workflowsConfig[workflowId].isoUrl)
|
|
1057
|
+
Underpost.baremetal.httpBootstrapServerStaticFactory({
|
|
1058
|
+
bootstrapHttpServerPath,
|
|
1059
|
+
hostname,
|
|
1060
|
+
cloudConfigSrc,
|
|
1061
|
+
kickstartSrc,
|
|
1062
|
+
isoUrl: workflowsConfig[workflowId].isoUrl,
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// Start HTTP bootstrap server if commissioning or if ISO URL is used (for ISO-based workflows).
|
|
1066
|
+
if (options.bootstrapHttpServerRun || options.commission) {
|
|
1067
|
+
Underpost.baremetal.httpBootstrapServerRunnerFactory({
|
|
1068
|
+
hostname,
|
|
1069
|
+
bootstrapHttpServerPath,
|
|
1070
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1071
|
+
port: options.bootstrapHttpServerPort,
|
|
1072
|
+
workflowId,
|
|
1073
|
+
workflowsConfig,
|
|
1074
|
+
}),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Rebuild NFS exports and the matching MAAS/firewalld host configuration.
|
|
1079
|
+
if (
|
|
1080
|
+
(options.nfsBuildServer === true || options.commission === true) &&
|
|
1081
|
+
(workflowsConfig[workflowId].type === 'iso-nfs' ||
|
|
1082
|
+
workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
|
|
1083
|
+
workflowsConfig[workflowId].type === 'chroot-container')
|
|
1084
|
+
)
|
|
1085
|
+
Underpost.baremetal.rebuildNfsServer({
|
|
1086
|
+
nfsHostPath,
|
|
1087
|
+
nfsReset: options.nfsReset,
|
|
1088
|
+
underpostRoot,
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// Handle commissioning tasks
|
|
1092
|
+
if (options.commission === true) {
|
|
1093
|
+
let { firmwares, networkInterfaceName, maas, menuentryStr, type } = workflowsConfig[workflowId];
|
|
1094
|
+
|
|
1095
|
+
// Use commissioning config (Ubuntu ephemeral) for PXE boot resources
|
|
1096
|
+
const commissioningImage = maas?.commissioning || {
|
|
1097
|
+
architecture: 'arm64/generic',
|
|
1098
|
+
name: 'ubuntu/noble',
|
|
1099
|
+
};
|
|
1100
|
+
const resource = resources.find(
|
|
1101
|
+
(o) => o.architecture === commissioningImage.architecture && o.name === commissioningImage.name,
|
|
1102
|
+
);
|
|
1103
|
+
logger.info('Commissioning resource', resource);
|
|
1104
|
+
|
|
1105
|
+
if (
|
|
1106
|
+
Underpost.baremetal.getFamilyBaseOs(workflowsConfig[workflowId].osIdLike).isDebianBased &&
|
|
1107
|
+
(type === 'iso-nfs' || type === 'chroot-debootstrap' || type === 'chroot-container')
|
|
1108
|
+
) {
|
|
1109
|
+
// Prepare NFS casper path if using NFS boot.
|
|
1110
|
+
shellExec(`mkdir -p ${nfsHostPath}/casper`);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Clean and create TFTP root path. The iPXE EFI binary is restored from
|
|
1114
|
+
// cache (or rebuilt) later by the gated build block, so no early copy here.
|
|
1115
|
+
shellExec(`sudo rm -rf ${tftpRootPath}`);
|
|
1116
|
+
shellExec(`mkdir -p ${tftpRootPath}/pxe`);
|
|
1117
|
+
|
|
1118
|
+
// Process firmwares for TFTP.
|
|
1119
|
+
for (const firmware of firmwares) {
|
|
1120
|
+
const { url, gateway, subnet } = firmware;
|
|
1121
|
+
if (url.match('.zip')) {
|
|
1122
|
+
const name = url.split('/').pop().replace('.zip', '');
|
|
1123
|
+
const path = `../${name}`;
|
|
1124
|
+
if (!fs.existsSync(path)) {
|
|
1125
|
+
await Downloader.downloadFile(url, `../${name}.zip`); // Download firmware if not exists.
|
|
1126
|
+
shellExec(`cd .. && mkdir ${name} && cd ${name} && unzip ../${name}.zip`); // Unzip firmware.
|
|
1127
|
+
}
|
|
1128
|
+
shellExec(`sudo cp -a ${path}/* ${tftpRootPath}`); // Copy firmware files to TFTP root.
|
|
1129
|
+
|
|
1130
|
+
if (gateway && subnet) {
|
|
1131
|
+
const bootConfSrc = Underpost.baremetal.bootConfFactory({
|
|
1132
|
+
workflowId,
|
|
1133
|
+
tftpIp: callbackMetaData.runnerHost.ip,
|
|
1134
|
+
tftpPrefixStr: tftpPrefix,
|
|
1135
|
+
macAddress,
|
|
1136
|
+
clientIp: ipAddress,
|
|
1137
|
+
subnet,
|
|
1138
|
+
gateway,
|
|
1139
|
+
});
|
|
1140
|
+
if (bootConfSrc) fs.writeFileSync(`${tftpRootPath}/boot_${name}.conf`, bootConfSrc, 'utf8');
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Configure GRUB for PXE boot.
|
|
1146
|
+
{
|
|
1147
|
+
// Fetch kernel and initrd paths from MAAS boot resource.
|
|
1148
|
+
// Both NFS and disk-based commissioning use MAAS boot resources.
|
|
1149
|
+
let kernelFilesPaths, resourcesPath;
|
|
1150
|
+
if (workflowsConfig[workflowId].type === 'chroot-container') {
|
|
1151
|
+
const arch = commissioningImage.architecture.split('/')[0];
|
|
1152
|
+
resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
|
|
1153
|
+
kernelFilesPaths = {
|
|
1154
|
+
'vmlinuz-efi': `${nfsHostPath}/boot/vmlinuz-efi`,
|
|
1155
|
+
'initrd.img': `${nfsHostPath}/boot/initrd.img`,
|
|
1156
|
+
};
|
|
1157
|
+
} else {
|
|
1158
|
+
const kf = Underpost.baremetal.kernelFactory({
|
|
1159
|
+
resource,
|
|
1160
|
+
type,
|
|
1161
|
+
nfsHostPath,
|
|
1162
|
+
isoUrl: options.isoUrl || workflowsConfig[workflowId].isoUrl,
|
|
1163
|
+
workflowId,
|
|
1164
|
+
});
|
|
1165
|
+
kernelFilesPaths = kf.kernelFilesPaths;
|
|
1166
|
+
resourcesPath = kf.resourcesPath;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const { cmd } = Underpost.baremetal.kernelCmdBootParamsFactory({
|
|
1170
|
+
ipClient: ipAddress,
|
|
1171
|
+
ipDhcpServer: callbackMetaData.runnerHost.ip,
|
|
1172
|
+
ipConfig,
|
|
1173
|
+
ipFileServer,
|
|
1174
|
+
netmask,
|
|
1175
|
+
hostname,
|
|
1176
|
+
dnsServer,
|
|
1177
|
+
networkInterfaceName,
|
|
1178
|
+
fileSystemUrl:
|
|
1179
|
+
type === 'iso-ram'
|
|
1180
|
+
? `http://${callbackMetaData.runnerHost.ip}:${Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1181
|
+
port: options.bootstrapHttpServerPort,
|
|
1182
|
+
workflowId,
|
|
1183
|
+
workflowsConfig,
|
|
1184
|
+
})}/${hostname}/${kernelFilesPaths.isoUrl.split('/').pop()}`
|
|
1185
|
+
: kernelFilesPaths.isoUrl,
|
|
1186
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1187
|
+
port: options.bootstrapHttpServerPort,
|
|
1188
|
+
workflowId,
|
|
1189
|
+
workflowsConfig,
|
|
1190
|
+
}),
|
|
1191
|
+
type,
|
|
1192
|
+
macAddress,
|
|
1193
|
+
cloudInit: options.cloudInit,
|
|
1194
|
+
dev: options.dev,
|
|
1195
|
+
osIdLike: workflowsConfig[workflowId].osIdLike || '',
|
|
1196
|
+
authCredentials,
|
|
1197
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// Check if iPXE mode is enabled AND the iPXE EFI binary exists
|
|
1201
|
+
let useIpxe = options.ipxe;
|
|
1202
|
+
if (options.ipxe) {
|
|
1203
|
+
const arch = commissioningImage.architecture.split('/')[0];
|
|
1204
|
+
const ipxeScript = Underpost.baremetal.ipxeScriptFactory({
|
|
1205
|
+
maasIp: callbackMetaData.runnerHost.ip,
|
|
1206
|
+
macAddress,
|
|
1207
|
+
architecture: arch,
|
|
1208
|
+
tftpPrefix,
|
|
1209
|
+
kernelCmd: cmd,
|
|
1210
|
+
});
|
|
1211
|
+
fs.writeFileSync(`${tftpRootPath}/stable-id.ipxe`, ipxeScript, 'utf8');
|
|
1212
|
+
|
|
1213
|
+
// Create embedded boot script that does DHCP and chains to the main script
|
|
1214
|
+
const embeddedScript = Underpost.baremetal.ipxeEmbeddedScriptFactory({
|
|
1215
|
+
tftpServer: callbackMetaData.runnerHost.ip,
|
|
1216
|
+
scriptPath: `/${tftpPrefix}/stable-id.ipxe`,
|
|
1217
|
+
macAddress: macAddress,
|
|
1218
|
+
});
|
|
1219
|
+
fs.writeFileSync(`${tftpRootPath}/boot.ipxe`, embeddedScript, 'utf8');
|
|
1220
|
+
|
|
1221
|
+
logger.info('✓ iPXE script generated for MAAS commissioning', {
|
|
1222
|
+
registeredMAC: macAddress,
|
|
1223
|
+
path: `${tftpRootPath}/stable-id.ipxe`,
|
|
1224
|
+
embeddedPath: `${tftpRootPath}/boot.ipxe`,
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
// iPXE EFI binary build policy:
|
|
1228
|
+
// --ipxe-rebuild -> always rebuild.
|
|
1229
|
+
// --ipxe (cache exists) -> reuse the cached binary, no build.
|
|
1230
|
+
// --ipxe (no cache) -> warn and build, then cache for reuse.
|
|
1231
|
+
const cachedIpxePath = `${ipxeCacheDir}/ipxe.efi`;
|
|
1232
|
+
const tftpIpxePath = `${tftpRootPath}/ipxe.efi`;
|
|
1233
|
+
if (options.ipxeRebuild || !fs.existsSync(cachedIpxePath)) {
|
|
1234
|
+
if (!options.ipxeRebuild) {
|
|
1235
|
+
logger.warn(
|
|
1236
|
+
'⚠ No cached iPXE EFI binary found — building now. Later runs reuse the cache; use --ipxe-rebuild to force a rebuild.',
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
Underpost.baremetal.ipxeEfiFactory({
|
|
1240
|
+
tftpRootPath,
|
|
1241
|
+
ipxeCacheDir,
|
|
1242
|
+
arch,
|
|
1243
|
+
underpostRoot,
|
|
1244
|
+
embeddedScriptPath: `${tftpRootPath}/boot.ipxe`,
|
|
1245
|
+
forceRebuild: true,
|
|
1246
|
+
});
|
|
1247
|
+
} else {
|
|
1248
|
+
shellExec(`cp ${cachedIpxePath} ${tftpIpxePath}`);
|
|
1249
|
+
logger.info('✓ Using cached iPXE EFI binary (pass --ipxe-rebuild to force a rebuild)', {
|
|
1250
|
+
path: cachedIpxePath,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const { grubCfgSrc } = Underpost.baremetal.grubFactory({
|
|
1256
|
+
menuentryStr,
|
|
1257
|
+
kernelPath: `/${tftpPrefix}/pxe/vmlinuz-efi`,
|
|
1258
|
+
initrdPath: `/${tftpPrefix}/pxe/initrd.img`,
|
|
1259
|
+
cmd,
|
|
1260
|
+
tftpIp: callbackMetaData.runnerHost.ip,
|
|
1261
|
+
ipxe: useIpxe,
|
|
1262
|
+
ipxePath: `/${tftpPrefix}/ipxe.efi`,
|
|
1263
|
+
});
|
|
1264
|
+
Underpost.baremetal.writeGrubConfigToFile({
|
|
1265
|
+
grubCfgSrc: machine ? grubCfgSrc.replaceAll('system-id', machine.system_id) : grubCfgSrc,
|
|
1266
|
+
});
|
|
1267
|
+
if (machine) {
|
|
1268
|
+
logger.info('✓ GRUB config written with system_id', { system_id: machine.system_id });
|
|
1269
|
+
}
|
|
1270
|
+
Underpost.baremetal.updateKernelFiles({
|
|
1271
|
+
commissioningImage,
|
|
1272
|
+
resourcesPath,
|
|
1273
|
+
tftpRootPath,
|
|
1274
|
+
kernelFilesPaths,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Pass architecture from commissioning or deployment config
|
|
1279
|
+
const grubArch = commissioningImage.architecture;
|
|
1280
|
+
Underpost.baremetal.efiGrubModulesFactory({ image: { architecture: grubArch } });
|
|
1281
|
+
|
|
1282
|
+
// Set ownership and permissions for TFTP root.
|
|
1283
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${process.env.TFTP_ROOT}`);
|
|
1284
|
+
shellExec(`sudo sudo chmod 755 ${process.env.TFTP_ROOT}`);
|
|
1285
|
+
|
|
1286
|
+
if (type === 'chroot-debootstrap' || type === 'chroot-container')
|
|
1287
|
+
await Underpost.baremetal.nfsMountCallback({
|
|
1288
|
+
hostname,
|
|
1289
|
+
nfsHostPath,
|
|
1290
|
+
workflowId,
|
|
1291
|
+
mount: true,
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
const commissionMonitorPayload = {
|
|
1295
|
+
macAddress,
|
|
1296
|
+
ipAddress,
|
|
1297
|
+
hostname,
|
|
1298
|
+
architecture: Underpost.baremetal.fallbackArchitecture(workflowsConfig[workflowId]),
|
|
1299
|
+
machine,
|
|
1300
|
+
};
|
|
1301
|
+
logger.info('Waiting for commissioning...', {
|
|
1302
|
+
...commissionMonitorPayload,
|
|
1303
|
+
machine: machine ? machine.system_id : null,
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
const { discovery, machine: discoveredMachine } =
|
|
1307
|
+
await Underpost.baremetal.commissionMonitor(commissionMonitorPayload);
|
|
1308
|
+
if (discoveredMachine) machine = discoveredMachine;
|
|
1309
|
+
|
|
1310
|
+
// Rocky/RHEL disk-install orchestration:
|
|
1311
|
+
// Once the ephemeral Kickstart/Anaconda runtime reports key-only SSH
|
|
1312
|
+
// readiness over the bootstrap HTTP POST sink, drive the unattended
|
|
1313
|
+
// install of Rocky onto the detected target disk via a key-only,
|
|
1314
|
+
// non-interactive remote command. The runtime also has an AUTO_INSTALL
|
|
1315
|
+
// fallback, so a missed handshake never blocks installation forever.
|
|
1316
|
+
if (
|
|
1317
|
+
Underpost.baremetal.getFamilyBaseOs(workflowsConfig[workflowId].osIdLike).isRhelBased &&
|
|
1318
|
+
options.remoteInstall !== false
|
|
1319
|
+
) {
|
|
1320
|
+
const { privateKeyPath, user: resolvedUser } = Underpost.baremetal.resolveSshKeyPaths({
|
|
1321
|
+
options,
|
|
1322
|
+
workflowsConfig,
|
|
1323
|
+
workflowId,
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
await Underpost.baremetal.remoteInstallOrchestrator({
|
|
1327
|
+
hostname,
|
|
1328
|
+
ipAddress,
|
|
1329
|
+
sshPort: 22,
|
|
1330
|
+
keyPath: privateKeyPath,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
// After the OS is installed and the node reboots into the deployed
|
|
1334
|
+
// disk, run the post-install dispatcher: it reads the workflow's
|
|
1335
|
+
// infraSetup field and provisions the requested infrastructure (e.g.
|
|
1336
|
+
// a kubeadm control-plane or worker) over key-only SSH. The resolved
|
|
1337
|
+
// user is used when authenticating to an EXISTING control-plane (e.g.
|
|
1338
|
+
// admin@dd-core); fresh nodes are always reached as root.
|
|
1339
|
+
await Underpost.baremetal.postInstallDispatcher({
|
|
1340
|
+
workflowId,
|
|
1341
|
+
workflowsConfig,
|
|
1342
|
+
hostname,
|
|
1343
|
+
ipAddress,
|
|
1344
|
+
options,
|
|
1345
|
+
underpostRoot,
|
|
1346
|
+
keyPath: privateKeyPath,
|
|
1347
|
+
controlUser: resolvedUser,
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* @method postInstallDispatcher
|
|
1355
|
+
* @description Generic first-boot post-install dispatcher. Reads the workflow's
|
|
1356
|
+
* `infraSetup` field and runs the matching infrastructure setup on the freshly
|
|
1357
|
+
* deployed node. Easy to extend: add a new case per supported infraSetup type.
|
|
1358
|
+
* Fails with a clear error if the workflow requests an unsupported type.
|
|
1359
|
+
* @param {object} params
|
|
1360
|
+
* @param {string} params.workflowId - Active workflow id.
|
|
1361
|
+
* @param {object} params.workflowsConfig - Loaded workflows config.
|
|
1362
|
+
* @param {string} params.hostname - Node hostname.
|
|
1363
|
+
* @param {string} params.ipAddress - Node IP (installed OS, key-only SSH).
|
|
1364
|
+
* @param {object} params.options - CLI options (carries --worker / --control).
|
|
1365
|
+
* @param {string} params.underpostRoot - Engine root for resolving scripts.
|
|
1366
|
+
* @param {string} [params.keyPath] - Private key path for key-only SSH.
|
|
1367
|
+
* @param {string} [params.controlUser='root'] - SSH user for an existing control-plane.
|
|
1368
|
+
* @returns {Promise<void>}
|
|
1369
|
+
* @memberof UnderpostBaremetal
|
|
1370
|
+
*/
|
|
1371
|
+
async postInstallDispatcher({
|
|
1372
|
+
workflowId,
|
|
1373
|
+
workflowsConfig,
|
|
1374
|
+
hostname,
|
|
1375
|
+
ipAddress,
|
|
1376
|
+
options,
|
|
1377
|
+
underpostRoot,
|
|
1378
|
+
keyPath,
|
|
1379
|
+
controlUser = 'root',
|
|
1380
|
+
}) {
|
|
1381
|
+
const infraSetup = workflowsConfig[workflowId]?.infraSetup;
|
|
1382
|
+
if (!infraSetup) {
|
|
1383
|
+
logger.info('No infraSetup configured for workflow; skipping post-install setup', { workflowId });
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
logger.info('Post-install dispatcher selecting infra setup', { workflowId, infraSetup });
|
|
1388
|
+
switch (infraSetup) {
|
|
1389
|
+
case 'underpost-kubeadm-contour':
|
|
1390
|
+
await Underpost.baremetal.infraSetupKubeadm({
|
|
1391
|
+
hostname,
|
|
1392
|
+
ipAddress,
|
|
1393
|
+
options,
|
|
1394
|
+
underpostRoot,
|
|
1395
|
+
keyPath,
|
|
1396
|
+
controlUser,
|
|
1397
|
+
});
|
|
1398
|
+
break;
|
|
1399
|
+
default:
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`Unsupported infraSetup "${infraSetup}" for workflow "${workflowId}". Supported: underpost-kubeadm-contour`,
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* @method infraSetupKubeadm
|
|
1408
|
+
* @description Provisions a kubeadm node on the freshly deployed machine over
|
|
1409
|
+
* key-only SSH. Control mode (default) initializes a new control-plane. Worker
|
|
1410
|
+
* mode (`--worker`) joins an existing cluster: the join token is retrieved
|
|
1411
|
+
* dynamically over SSH from the control-plane node (`--control <ip>`) — never
|
|
1412
|
+
* pasted manually — and passed to the node setup script.
|
|
1413
|
+
* @param {object} params
|
|
1414
|
+
* @param {string} params.hostname - Node hostname (logging).
|
|
1415
|
+
* @param {string} params.ipAddress - Target node IP (installed OS).
|
|
1416
|
+
* @param {object} params.options - CLI options; options.worker, options.control.
|
|
1417
|
+
* @param {string} params.underpostRoot - Engine root for resolving the script.
|
|
1418
|
+
* @param {string} [params.keyPath] - Private key path for key-only SSH.
|
|
1419
|
+
* @param {string} [params.controlUser='root'] - SSH login user for an existing control-plane (e.g. admin).
|
|
1420
|
+
* @returns {Promise<void>}
|
|
1421
|
+
* @memberof UnderpostBaremetal
|
|
1422
|
+
*/
|
|
1423
|
+
async infraSetupKubeadm({
|
|
1424
|
+
hostname,
|
|
1425
|
+
ipAddress,
|
|
1426
|
+
options,
|
|
1427
|
+
underpostRoot,
|
|
1428
|
+
keyPath = 'engine-private/deploy/id_rsa',
|
|
1429
|
+
controlUser = 'root',
|
|
1430
|
+
}) {
|
|
1431
|
+
const role = options.worker ? 'worker' : 'control';
|
|
1432
|
+
const scriptPath = `${underpostRoot}/scripts/kubeadm-node-setup.sh`;
|
|
1433
|
+
const installedOsTimeoutMs = 15 * 60 * 1000;
|
|
1434
|
+
|
|
1435
|
+
logger.info(`Kubeadm post-install setup (${role}) on ${hostname} (${ipAddress})`);
|
|
1436
|
+
|
|
1437
|
+
if (!options.resumeInfraSetup && !options.resumeJoin) {
|
|
1438
|
+
// The node is rebooting from the ephemeral installer into the deployed OS.
|
|
1439
|
+
// First wait for the (ephemeral) SSH port to CLOSE so we don't latch onto
|
|
1440
|
+
// the pre-reboot sshd, then wait for the installed OS sshd to come UP.
|
|
1441
|
+
logger.info('Waiting for node to reboot into the deployed OS (port close → reopen)...', { ipAddress });
|
|
1442
|
+
await Underpost.ssh.waitForSshPortClosed({ host: ipAddress, port: 22, timeoutMs: 4 * 60 * 1000 });
|
|
1443
|
+
const reachable = await Underpost.ssh.waitForSshPort({
|
|
1444
|
+
host: ipAddress,
|
|
1445
|
+
port: 22,
|
|
1446
|
+
timeoutMs: installedOsTimeoutMs,
|
|
1447
|
+
});
|
|
1448
|
+
if (!reachable) {
|
|
1449
|
+
logger.error('Installed OS SSH not reachable after reboot; cannot run kubeadm setup', { ipAddress });
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
// The first time the port opens the node is still early in boot and
|
|
1453
|
+
// NetworkManager may re-apply the static profile, resetting in-flight TCP
|
|
1454
|
+
// connections. Let the network settle before driving the long node setup.
|
|
1455
|
+
logger.info('Deployed OS SSH is up; letting the network settle before node setup...', { ipAddress });
|
|
1456
|
+
await timer(30000);
|
|
1457
|
+
} else {
|
|
1458
|
+
logger.info('--resume-infra-setup: SSH already up, skipping port close/reopen wait', { ipAddress });
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
let scriptArgs;
|
|
1462
|
+
if (role === 'worker') {
|
|
1463
|
+
const controlIp = options.control;
|
|
1464
|
+
if (!controlIp || typeof controlIp !== 'string') {
|
|
1465
|
+
throw new Error('Worker mode requires the control-plane IP via --control <ip>');
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Retrieve the join command dynamically from the control-plane node over
|
|
1469
|
+
// SSH (reuses the same key-only SSH execution path). No manual token paste.
|
|
1470
|
+
// The control-plane may be an existing deploy reached as a non-root user
|
|
1471
|
+
// (e.g. admin@dd-core), selected via --user/--deploy-id.
|
|
1472
|
+
logger.info('Retrieving kubeadm join command from control-plane over SSH', {
|
|
1473
|
+
controlIp,
|
|
1474
|
+
controlUser,
|
|
1475
|
+
keyPath,
|
|
1476
|
+
});
|
|
1477
|
+
const joinResult = await Underpost.ssh.sshExecBatch({
|
|
1478
|
+
host: controlIp,
|
|
1479
|
+
port: 22,
|
|
1480
|
+
user: controlUser,
|
|
1481
|
+
keyPath,
|
|
1482
|
+
retries: 4,
|
|
1483
|
+
waitForPortMs: 5 * 60 * 1000,
|
|
1484
|
+
command: 'sudo kubeadm token create --print-join-command',
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
let joinCommand = (joinResult.stdout || '')
|
|
1488
|
+
.split('\n')
|
|
1489
|
+
.map((l) => l.trim())
|
|
1490
|
+
.find((l) => l.startsWith('kubeadm join'));
|
|
1491
|
+
if (!joinResult.ok || !joinCommand) {
|
|
1492
|
+
throw new Error(
|
|
1493
|
+
`Failed to retrieve kubeadm join command from control-plane ${controlUser}@${controlIp} using key "${keyPath}" (code ${joinResult.code}). ` +
|
|
1494
|
+
`Ensure that key authorizes ${controlUser}@${controlIp} — pass --deploy-id <id> --user <user> (key from engine-private/conf/<id>/users/<user>) ` +
|
|
1495
|
+
`or --ssh-key-dir <dir> to use the key/user the control-plane accepts. stderr: ${joinResult.stderr.slice(-300)}`,
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
// kubeadm prints the API endpoint as the control-plane's hostname (often
|
|
1499
|
+
// localhost.localdomain), which the worker cannot resolve/route. Capture
|
|
1500
|
+
// that original endpoint host (so the worker can map it -> control IP in
|
|
1501
|
+
// /etc/hosts; the cluster-info / kubeadm-config ConfigMaps reference it),
|
|
1502
|
+
// then rewrite the join positional endpoint to the real control-plane IP.
|
|
1503
|
+
const endpointMatch = joinCommand.match(/kubeadm join\s+([^\s:]+):\d+/);
|
|
1504
|
+
const endpointHost = endpointMatch ? endpointMatch[1] : '';
|
|
1505
|
+
joinCommand = joinCommand.replace(/kubeadm join\s+\S+:(\d+)/, `kubeadm join ${controlIp}:$1`);
|
|
1506
|
+
logger.info('✓ Retrieved kubeadm join command from control-plane', { controlIp, endpointHost, joinCommand });
|
|
1507
|
+
scriptArgs = `--worker --join-command="${joinCommand}" --control-ip=${controlIp}`;
|
|
1508
|
+
if (endpointHost && endpointHost !== controlIp) {
|
|
1509
|
+
scriptArgs += ` --control-endpoint-host=${endpointHost}`;
|
|
1510
|
+
}
|
|
1511
|
+
// --resume-join: skip all node prep and go straight to the kubeadm join.
|
|
1512
|
+
if (options.resumeJoin) scriptArgs += ' --join-only';
|
|
1513
|
+
} else {
|
|
1514
|
+
scriptArgs = '--control';
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Secrets needed on the node to clone the private engine-private repo
|
|
1518
|
+
// (engine-private/conf/.../.env.production for `node bin run secret`).
|
|
1519
|
+
const githubUsername = Underpost.baremetal.readEngineConfig('GITHUB_USERNAME') || 'underpostnet';
|
|
1520
|
+
const githubEnv = {
|
|
1521
|
+
GITHUB_TOKEN: Underpost.baremetal.readEngineConfig('GITHUB_TOKEN'),
|
|
1522
|
+
GITHUB_USERNAME: githubUsername,
|
|
1523
|
+
};
|
|
1524
|
+
if (!githubEnv.GITHUB_TOKEN) {
|
|
1525
|
+
logger.warn('GITHUB_TOKEN not resolved on controller; engine-private clone on the node will be skipped');
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Resolve the engine + engine-private repos the node clones and normalizes
|
|
1529
|
+
// to /home/dd/engine and /home/dd/engine/engine-private. CLI overrides win;
|
|
1530
|
+
// otherwise default to the base engine and the deployId's private repo
|
|
1531
|
+
// (engine-<id>-private), mirroring repository.privateEngineRepoFactory.
|
|
1532
|
+
const deployIdSuffix = options.deployId ? options.deployId.split('-')[1] : '';
|
|
1533
|
+
const engineRepo = options.engineRepo || `https://github.com/${githubUsername}/engine.git`;
|
|
1534
|
+
const enginePrivateRepo =
|
|
1535
|
+
options.enginePrivateRepo ||
|
|
1536
|
+
(deployIdSuffix
|
|
1537
|
+
? `https://github.com/${githubUsername}/engine-${deployIdSuffix}-private.git`
|
|
1538
|
+
: `https://github.com/${githubUsername}/engine-private.git`);
|
|
1539
|
+
const repoArgs = [
|
|
1540
|
+
`--engine-repo=${engineRepo}`,
|
|
1541
|
+
`--engine-private-repo=${enginePrivateRepo}`,
|
|
1542
|
+
...(options.engineBranch ? [`--engine-branch=${options.engineBranch}`] : []),
|
|
1543
|
+
...(options.enginePrivateBranch ? [`--engine-private-branch=${options.enginePrivateBranch}`] : []),
|
|
1544
|
+
].join(' ');
|
|
1545
|
+
scriptArgs = `${scriptArgs} ${repoArgs}`.trim();
|
|
1546
|
+
logger.info('Node engine repos', { engineRepo, enginePrivateRepo });
|
|
1547
|
+
|
|
1548
|
+
logger.info(`Running kubeadm-node-setup.sh (${role}) on ${ipAddress}... (this can take several minutes)`);
|
|
1549
|
+
// The node setup is a long, mostly-idempotent install. Keep retries low so
|
|
1550
|
+
// a late failure doesn't re-run the whole thing many times; the script
|
|
1551
|
+
// skips already-completed steps (Node, engine clone) on a re-run.
|
|
1552
|
+
const result = await Underpost.ssh.sshRunScript({
|
|
1553
|
+
host: ipAddress,
|
|
1554
|
+
scriptPath,
|
|
1555
|
+
args: scriptArgs,
|
|
1556
|
+
env: githubEnv,
|
|
1557
|
+
remotePath: '/tmp/kubeadm-node-setup.sh',
|
|
1558
|
+
keyPath,
|
|
1559
|
+
retries: 2,
|
|
1560
|
+
waitForPortMs: 5 * 60 * 1000,
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
if (result.ok) {
|
|
1564
|
+
logger.info(`✓ Kubeadm ${role} node setup completed on ${hostname}`, {
|
|
1565
|
+
stdout: result.stdout.split('\n').slice(-12).join('\n'),
|
|
1566
|
+
});
|
|
1567
|
+
} else {
|
|
1568
|
+
logger.error(`✗ Kubeadm ${role} node setup failed on ${hostname}`, {
|
|
1569
|
+
code: result.code,
|
|
1570
|
+
stderr: result.stderr.slice(-600),
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* @method remoteInstallOrchestrator
|
|
1577
|
+
* @description Waits for the ephemeral runtime's 'ssh-ready' lifecycle event,
|
|
1578
|
+
* then triggers the unattended Rocky install over a key-only SSH batch
|
|
1579
|
+
* command. Returns structured success/failure information. Non-fatal: logs
|
|
1580
|
+
* and returns on failure so the runtime's AUTO_INSTALL fallback can proceed.
|
|
1581
|
+
* @param {object} params
|
|
1582
|
+
* @param {string} params.hostname - Hostname key used by the bootstrap POST sink.
|
|
1583
|
+
* @param {string} params.ipAddress - Fallback IP if the runtime did not report one.
|
|
1584
|
+
* @param {number} [params.sshPort=22] - SSH port of the ephemeral runtime.
|
|
1585
|
+
* @param {string} [params.keyPath] - Private key path for key-only auth.
|
|
1586
|
+
* @returns {Promise<{ok: boolean, host: string, result?: object, reason?: string}>}
|
|
1587
|
+
* @memberof UnderpostBaremetal
|
|
1588
|
+
*/
|
|
1589
|
+
async remoteInstallOrchestrator({ hostname, ipAddress, sshPort = 22, keyPath = 'engine-private/deploy/id_rsa' }) {
|
|
1590
|
+
logger.info('Awaiting ephemeral runtime ssh-ready handshake...', { hostname, ipAddress });
|
|
1591
|
+
const readyEvent = await Underpost.baremetal.waitForBootstrapStage({ hostname, stage: 'ssh-ready' });
|
|
1592
|
+
|
|
1593
|
+
const host = readyEvent?.ip && readyEvent.ip !== 'UNKNOWN' ? readyEvent.ip : ipAddress;
|
|
1594
|
+
if (!readyEvent) {
|
|
1595
|
+
logger.warn('No ssh-ready event received; relying on runtime AUTO_INSTALL fallback', { host });
|
|
1596
|
+
return { ok: false, host, reason: 'no ssh-ready event' };
|
|
1597
|
+
}
|
|
1598
|
+
logger.info('SSH-ready event received', { host, metadata: readyEvent });
|
|
1599
|
+
|
|
1600
|
+
// Key-only, non-interactive trigger of the on-host installer. We only DROP
|
|
1601
|
+
// the trigger file here; the persistent %pre lifecycle loop (a child of
|
|
1602
|
+
// Anaconda, not of this sshd session) launches the installer fully
|
|
1603
|
+
// detached. Running the installer directly as an sshd child causes it to
|
|
1604
|
+
// be torn down when the SSH channel closes, before it can even start.
|
|
1605
|
+
const result = await Underpost.ssh.sshExecBatch({
|
|
1606
|
+
host,
|
|
1607
|
+
port: sshPort,
|
|
1608
|
+
user: 'root',
|
|
1609
|
+
keyPath,
|
|
1610
|
+
waitForPortMs: 5 * 60 * 1000,
|
|
1611
|
+
retries: 4,
|
|
1612
|
+
command: ['set -e', 'touch /tmp/.underpost-install-trigger', 'echo "install trigger dropped"'].join('\n'),
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
if (result.ok) {
|
|
1616
|
+
logger.info('✓ Remote install trigger dropped', { host, stdout: result.stdout.trim() });
|
|
1617
|
+
} else {
|
|
1618
|
+
logger.error('Remote install trigger failed; runtime AUTO_INSTALL fallback will cover it', {
|
|
1619
|
+
host,
|
|
1620
|
+
code: result.code,
|
|
1621
|
+
stderr: result.stderr.slice(-400),
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Follow the install lifecycle so the controller reflects real progress.
|
|
1626
|
+
const started = await Underpost.baremetal.waitForBootstrapStage({
|
|
1627
|
+
hostname,
|
|
1628
|
+
stage: 'install-start',
|
|
1629
|
+
timeoutMs: 3 * 60 * 1000,
|
|
1630
|
+
});
|
|
1631
|
+
if (!started) {
|
|
1632
|
+
logger.warn('Installer did not report install-start within 3m; check /tmp/underpost-install.log on the node', {
|
|
1633
|
+
host,
|
|
1634
|
+
});
|
|
1635
|
+
return { ok: false, host, reason: 'no install-start', result };
|
|
1636
|
+
}
|
|
1637
|
+
logger.info('Installer launched on node', { host });
|
|
1638
|
+
|
|
1639
|
+
// Wait for a terminal stage. The node reboots on success, so 'completed'
|
|
1640
|
+
// is the last event we will receive.
|
|
1641
|
+
const terminal = await Underpost.baremetal.waitForBootstrapStage({
|
|
1642
|
+
hostname,
|
|
1643
|
+
stage: 'completed',
|
|
1644
|
+
timeoutMs: 40 * 60 * 1000,
|
|
1645
|
+
});
|
|
1646
|
+
if (terminal) {
|
|
1647
|
+
logger.info('✓ Rocky install completed; node rebooting into deployed OS', { host, event: terminal });
|
|
1648
|
+
return { ok: true, host, result, terminal };
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const failed = (UnderpostBaremetal.bootstrapStatusEvents.get(hostname) || []).find((e) => e.stage === 'failed');
|
|
1652
|
+
if (failed) {
|
|
1653
|
+
logger.error('✗ Rocky install reported failure', { host, event: failed });
|
|
1654
|
+
return { ok: false, host, reason: 'install failed', result, failed };
|
|
1655
|
+
}
|
|
1656
|
+
logger.warn('Install did not reach a terminal stage within timeout', { host });
|
|
1657
|
+
return { ok: false, host, reason: 'no terminal stage', result };
|
|
1658
|
+
},
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* @method installPacker
|
|
1662
|
+
* @description Installs Packer CLI.
|
|
1663
|
+
* @memberof UnderpostBaremetal
|
|
1664
|
+
* @returns {Promise<void>}
|
|
1665
|
+
*/
|
|
1666
|
+
async installPacker(underpostRoot) {
|
|
1667
|
+
const scriptPath = `${underpostRoot}/scripts/packer-setup.sh`;
|
|
1668
|
+
logger.info(`Installing Packer using script: ${scriptPath}`);
|
|
1669
|
+
shellExec(`sudo chmod +x ${scriptPath}`);
|
|
1670
|
+
shellExec(`sudo ${scriptPath}`);
|
|
1671
|
+
},
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* @method ipxeBuildIso
|
|
1675
|
+
* @description Builds a UEFI-bootable iPXE ISO with an embedded bridge script.
|
|
1676
|
+
* @param {object} params
|
|
1677
|
+
* @param {string} params.workflowId - The workflow identifier (e.g., 'hp-envy-iso-ram').
|
|
1678
|
+
* @param {string} params.isoOutputPath - Output path for the generated ISO file.
|
|
1679
|
+
* @param {string} params.tftpPrefix - TFTP prefix directory (e.g., 'envy').
|
|
1680
|
+
* @param {string} params.ipFileServer - IP address of the TFTP/file server to chain to.
|
|
1681
|
+
* @param {string} [params.ipAddress='192.168.1.191'] - The IP address of the client machine.
|
|
1682
|
+
* @param {string} [params.ipConfig='none'] - IP configuration method (e.g., 'dhcp', 'none').
|
|
1683
|
+
* @param {string} [params.netmask='255.255.255.0'] - The network mask.
|
|
1684
|
+
* @param {string} [params.dnsServer='8.8.8.8'] - The DNS server address.
|
|
1685
|
+
* @param {string} [params.macAddress=''] - The MAC address of the client machine.
|
|
1686
|
+
* @param {boolean} [params.cloudInit=false] - Flag to enable cloud-init.
|
|
1687
|
+
* @param {boolean} [params.dev=false] - Development mode flag to determine paths.
|
|
1688
|
+
* @param {boolean} [params.forceRebuild=false] - Force a complete iPXE rebuild. Without this, reuses existing ISO.
|
|
1689
|
+
* @param {number} [params.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server used in ISO RAM workflows.
|
|
1690
|
+
* @memberof UnderpostBaremetal
|
|
1691
|
+
* @returns {Promise<void>}
|
|
1692
|
+
*/
|
|
1693
|
+
async ipxeBuildIso({
|
|
1694
|
+
workflowId,
|
|
1695
|
+
isoOutputPath,
|
|
1696
|
+
tftpPrefix,
|
|
1697
|
+
ipFileServer,
|
|
1698
|
+
ipAddress,
|
|
1699
|
+
ipConfig,
|
|
1700
|
+
netmask,
|
|
1701
|
+
dnsServer,
|
|
1702
|
+
macAddress,
|
|
1703
|
+
cloudInit,
|
|
1704
|
+
dev,
|
|
1705
|
+
forceRebuild = false,
|
|
1706
|
+
bootstrapHttpServerPort,
|
|
1707
|
+
}) {
|
|
1708
|
+
const outputPath = !isoOutputPath || isoOutputPath === '.' ? `./ipxe-${workflowId}.iso` : isoOutputPath;
|
|
1709
|
+
shellExec(`mkdir -p $(dirname ${outputPath})`);
|
|
1710
|
+
|
|
1711
|
+
const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
|
|
1712
|
+
if (!workflowsConfig[workflowId]) {
|
|
1713
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const authCredentials = cloudInit
|
|
1717
|
+
? Underpost.baremetal.maasAuthCredentialsFactory()
|
|
1718
|
+
: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' };
|
|
1719
|
+
|
|
1720
|
+
const { cmd } = Underpost.baremetal.kernelCmdBootParamsFactory({
|
|
1721
|
+
ipClient: ipAddress,
|
|
1722
|
+
ipDhcpServer: ipFileServer,
|
|
1723
|
+
ipFileServer,
|
|
1724
|
+
ipConfig,
|
|
1725
|
+
netmask,
|
|
1726
|
+
hostname: workflowId,
|
|
1727
|
+
dnsServer,
|
|
1728
|
+
fileSystemUrl:
|
|
1729
|
+
dev && workflowsConfig[workflowId].type === 'iso-ram'
|
|
1730
|
+
? `http://${ipFileServer}:${Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1731
|
+
port: bootstrapHttpServerPort,
|
|
1732
|
+
workflowId,
|
|
1733
|
+
workflowsConfig,
|
|
1734
|
+
})}/${workflowId}/${workflowsConfig[workflowId].isoUrl.split('/').pop()}`
|
|
1735
|
+
: workflowsConfig[workflowId].isoUrl,
|
|
1736
|
+
type: workflowsConfig[workflowId].type,
|
|
1737
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
1738
|
+
macAddress,
|
|
1739
|
+
cloudInit,
|
|
1740
|
+
osIdLike: workflowsConfig[workflowId].osIdLike,
|
|
1741
|
+
networkInterfaceName: workflowsConfig[workflowId].networkInterfaceName,
|
|
1742
|
+
authCredentials,
|
|
1743
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1744
|
+
port: bootstrapHttpServerPort,
|
|
1745
|
+
workflowId,
|
|
1746
|
+
workflowsConfig,
|
|
1747
|
+
}),
|
|
1748
|
+
dev,
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
const ipxeSrcDir = '/home/dd/ipxe/src';
|
|
1752
|
+
const embedScriptName = `embed_${workflowId}.ipxe`;
|
|
1753
|
+
const embedScriptPath = path.join(ipxeSrcDir, embedScriptName);
|
|
1754
|
+
|
|
1755
|
+
const embedScriptContent = Underpost.baremetal.ipxeScriptFactory({
|
|
1756
|
+
maasIp: ipFileServer,
|
|
1757
|
+
tftpPrefix,
|
|
1758
|
+
kernelCmd: cmd,
|
|
1759
|
+
minimal: true,
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
fs.writeFileSync(embedScriptPath, embedScriptContent);
|
|
1763
|
+
logger.info(`Created embedded script at ${embedScriptPath}`);
|
|
1764
|
+
|
|
1765
|
+
// Determine target architecture
|
|
1766
|
+
let targetArch = 'x86_64';
|
|
1767
|
+
if (
|
|
1768
|
+
workflowsConfig[workflowId].architecture === 'arm64' ||
|
|
1769
|
+
workflowsConfig[workflowId].architecture === 'aarch64'
|
|
1770
|
+
) {
|
|
1771
|
+
targetArch = 'arm64';
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const platformDir = targetArch === 'arm64' ? 'bin-arm64-efi' : 'bin-x86_64-efi';
|
|
1775
|
+
const makeTarget = `${platformDir}/ipxe.iso`;
|
|
1776
|
+
const builtIsoPath = path.join(ipxeSrcDir, makeTarget);
|
|
1777
|
+
|
|
1778
|
+
if (!forceRebuild && fs.existsSync(builtIsoPath)) {
|
|
1779
|
+
fs.copySync(builtIsoPath, outputPath);
|
|
1780
|
+
logger.info(`Reusing existing iPXE ISO: ${builtIsoPath} -> ${outputPath}`);
|
|
1781
|
+
} else {
|
|
1782
|
+
logger.info(`Rebuild: cleaning iPXE build artifacts...`);
|
|
1783
|
+
shellExec(`cd ${ipxeSrcDir} && make clean`);
|
|
1784
|
+
|
|
1785
|
+
const hostArch = process.arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
1786
|
+
let crossCompile = '';
|
|
1787
|
+
if (hostArch === 'x86_64' && targetArch === 'arm64') {
|
|
1788
|
+
crossCompile = 'CROSS_COMPILE=aarch64-linux-gnu-';
|
|
1789
|
+
} else if (hostArch === 'arm64' && targetArch === 'x86_64') {
|
|
1790
|
+
crossCompile = 'CROSS_COMPILE=x86_64-linux-gnu-';
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
logger.info(
|
|
1794
|
+
`Building iPXE ISO for ${targetArch} on ${hostArch}: make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`,
|
|
1795
|
+
);
|
|
1796
|
+
|
|
1797
|
+
const buildCmd = `cd ${ipxeSrcDir} && make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`;
|
|
1798
|
+
shellExec(buildCmd);
|
|
1799
|
+
|
|
1800
|
+
if (fs.existsSync(builtIsoPath)) {
|
|
1801
|
+
fs.copySync(builtIsoPath, outputPath);
|
|
1802
|
+
logger.info(`ISO successfully built and copied to ${outputPath}`);
|
|
1803
|
+
} else {
|
|
1804
|
+
logger.error(`Failed to build ISO at ${builtIsoPath}`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* @method fallbackArchitecture
|
|
1811
|
+
* @description Determines the architecture to use for boot resources, with a fallback mechanism.
|
|
1812
|
+
* @param {object} workflowsConfig - The configuration object for the current workflow.
|
|
1813
|
+
* @returns {string} The architecture string (e.g., 'arm64', 'amd64') to use for boot resources.
|
|
1814
|
+
* @memberof UnderpostBaremetal
|
|
1815
|
+
*/
|
|
1816
|
+
fallbackArchitecture(workflowsConfig) {
|
|
1817
|
+
return (
|
|
1818
|
+
workflowsConfig.architecture ||
|
|
1819
|
+
workflowsConfig.maas?.commissioning?.architecture ||
|
|
1820
|
+
workflowsConfig.container?.architecture ||
|
|
1821
|
+
workflowsConfig.debootstrap?.image?.architecture
|
|
1822
|
+
);
|
|
1823
|
+
},
|
|
1824
|
+
|
|
1825
|
+
/**
|
|
1826
|
+
* @method macAddressFactory
|
|
1827
|
+
* @description Generates or returns a MAC address based on options.
|
|
1828
|
+
* @param {object} options - Options for MAC address generation.
|
|
1829
|
+
* @param {string} options.mac - 'random' for random MAC, 'hardware' to use device's actual MAC, specific MAC string, or empty for default.
|
|
1830
|
+
* @returns {object} Object with mac property - null for 'hardware', generated/specified MAC otherwise.
|
|
1831
|
+
* @memberof UnderpostBaremetal
|
|
1832
|
+
*/
|
|
1833
|
+
macAddressFactory(options = { mac: '' }) {
|
|
1834
|
+
const len = 6;
|
|
1835
|
+
const defaultMac = range(1, len)
|
|
1836
|
+
.map(() => '00')
|
|
1837
|
+
.join(':');
|
|
1838
|
+
if (options) {
|
|
1839
|
+
if (!options.mac) options.mac = defaultMac;
|
|
1840
|
+
if (options.mac === 'hardware') {
|
|
1841
|
+
// Return null to indicate hardware MAC should be used (no spoofing)
|
|
1842
|
+
options.mac = null;
|
|
1843
|
+
} else if (options.mac === 'random') {
|
|
1844
|
+
options.mac = range(1, len)
|
|
1845
|
+
.map(() => s4().toLowerCase().substring(0, 2))
|
|
1846
|
+
.join(':');
|
|
1847
|
+
}
|
|
1848
|
+
} else options = { mac: defaultMac };
|
|
1849
|
+
return options;
|
|
1850
|
+
},
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* @method downloadISO
|
|
1854
|
+
* @description Downloads a generic ISO and extracts kernel boot files.
|
|
1855
|
+
* @param {object} params - Parameters for the method.
|
|
1856
|
+
* @param {object} params.resource - The MAAS boot resource object.
|
|
1857
|
+
* @param {string} params.architecture - The architecture (arm64 or amd64).
|
|
1858
|
+
* @param {string} params.nfsHostPath - The NFS host path to store the ISO and extracted files.
|
|
1859
|
+
* @param {string} params.isoUrl - The full URL to the ISO file to download.
|
|
1860
|
+
* @param {string} params.osIdLike - OS family identifier (e.g., 'debian ubuntu' or 'rhel centos fedora').
|
|
1861
|
+
* @returns {object} An object containing paths to the extracted kernel, initrd, and optionally squashfs.
|
|
1862
|
+
* @memberof UnderpostBaremetal
|
|
1863
|
+
*/
|
|
1864
|
+
downloadISO({ resource, architecture, nfsHostPath, isoUrl, osIdLike }) {
|
|
1865
|
+
const arch = architecture || resource.architecture.split('/')[0];
|
|
1866
|
+
|
|
1867
|
+
// Validate that isoUrl is provided
|
|
1868
|
+
if (!isoUrl) {
|
|
1869
|
+
throw new Error('isoUrl parameter is required. Please specify the full ISO URL in the workflow configuration.');
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Extract ISO filename from URL
|
|
1873
|
+
const isoFilename = isoUrl.split('/').pop();
|
|
1874
|
+
|
|
1875
|
+
// Determine OS family from osIdLike
|
|
1876
|
+
const { isDebianBased, isRhelBased } = Underpost.baremetal.getFamilyBaseOs(osIdLike);
|
|
1877
|
+
|
|
1878
|
+
// Set extraction directory based on OS family
|
|
1879
|
+
const extractDirName = isDebianBased ? 'casper' : 'iso-extract';
|
|
1880
|
+
shellExec(`mkdir -p ${nfsHostPath}/${extractDirName}`);
|
|
1881
|
+
|
|
1882
|
+
const isoPath = `/var/tmp/live-iso/${isoFilename}`;
|
|
1883
|
+
const extractDir = `${nfsHostPath}/${extractDirName}`;
|
|
1884
|
+
|
|
1885
|
+
if (!fs.existsSync(isoPath)) {
|
|
1886
|
+
logger.info(`Downloading ISO for ${arch}...`);
|
|
1887
|
+
logger.info(`URL: ${isoUrl}`);
|
|
1888
|
+
shellExec(`mkdir -p /var/tmp/live-iso`);
|
|
1889
|
+
shellExec(`wget --progress=bar:force -O ${isoPath} "${isoUrl}"`);
|
|
1890
|
+
// Verify download by checking file existence and size (not exit code, which can be unreliable)
|
|
1891
|
+
if (!fs.existsSync(isoPath)) {
|
|
1892
|
+
throw new Error(`Failed to download ISO from ${isoUrl} - file not created`);
|
|
1893
|
+
}
|
|
1894
|
+
const stats = fs.statSync(isoPath);
|
|
1895
|
+
if (stats.size < 100 * 1024 * 1024) {
|
|
1896
|
+
shellExec(`rm -f ${isoPath}`);
|
|
1897
|
+
throw new Error(`Downloaded ISO is too small (${stats.size} bytes), download may have failed`);
|
|
1898
|
+
}
|
|
1899
|
+
logger.info(`Downloaded ISO to ${isoPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// ISO Logic
|
|
1903
|
+
const mountPoint = `${nfsHostPath}/mnt-iso-${arch}`;
|
|
1904
|
+
shellExec(`mkdir -p ${mountPoint}`);
|
|
1905
|
+
|
|
1906
|
+
// Ensure mount point is not already mounted
|
|
1907
|
+
shellExec(`sudo umount ${mountPoint}`, {
|
|
1908
|
+
silentOnError: true, // Ignore errors if not mounted
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
try {
|
|
1912
|
+
// Mount the ISO
|
|
1913
|
+
shellExec(`sudo mount -o loop,ro ${isoPath} ${mountPoint}`);
|
|
1914
|
+
logger.info(`Mounted ISO at ${mountPoint}`);
|
|
1915
|
+
|
|
1916
|
+
// Distribution-specific extraction logic
|
|
1917
|
+
if (isDebianBased) {
|
|
1918
|
+
// Ubuntu/Debian: Extract from casper directory
|
|
1919
|
+
if (!fs.existsSync(`${mountPoint}/casper`)) {
|
|
1920
|
+
throw new Error(`Failed to mount ISO or casper directory not found: ${isoPath}`);
|
|
1921
|
+
}
|
|
1922
|
+
logger.info(`Checking casper directory contents...`);
|
|
1923
|
+
shellExec(`ls -la ${mountPoint}/casper/ 2>/dev/null || echo "casper directory not found"`);
|
|
1924
|
+
shellExec(`sudo cp -a ${mountPoint}/casper/* ${extractDir}/`);
|
|
1925
|
+
} else if (isRhelBased) {
|
|
1926
|
+
// RHEL/Rocky: Extract from images/pxeboot directory
|
|
1927
|
+
const pxebootDir = `${mountPoint}/images/pxeboot`;
|
|
1928
|
+
if (!fs.existsSync(pxebootDir)) {
|
|
1929
|
+
throw new Error(`Failed to mount ISO or images/pxeboot directory not found: ${isoPath}`);
|
|
1930
|
+
}
|
|
1931
|
+
logger.info(`Extracting kernel and initrd from ${pxebootDir}...`);
|
|
1932
|
+
shellExec(`sudo cp -a ${pxebootDir}/vmlinuz ${extractDir}/vmlinuz`);
|
|
1933
|
+
shellExec(`sudo cp -a ${pxebootDir}/initrd.img ${extractDir}/initrd`);
|
|
1934
|
+
}
|
|
1935
|
+
} finally {
|
|
1936
|
+
shellExec(`ls -la ${mountPoint}/`);
|
|
1937
|
+
|
|
1938
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${extractDir}`);
|
|
1939
|
+
// Unmount ISO
|
|
1940
|
+
shellExec(`sudo umount ${mountPoint}`);
|
|
1941
|
+
logger.info(`Unmounted ISO`);
|
|
1942
|
+
// Clean up temporary mount point
|
|
1943
|
+
shellExec(`rmdir ${mountPoint}`);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
return {
|
|
1947
|
+
'vmlinuz-efi': `${extractDir}/vmlinuz`,
|
|
1948
|
+
'initrd.img': `${extractDir}/initrd`,
|
|
1949
|
+
isoUrl,
|
|
1950
|
+
};
|
|
1951
|
+
},
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* @method machineFactory
|
|
1955
|
+
* @description Creates a new machine in MAAS with specified options.
|
|
1956
|
+
* @param {object} options - Options for creating the machine.
|
|
1957
|
+
* @param {string} options.macAddress - The MAC address of the machine.
|
|
1958
|
+
* @param {string} options.hostname - The hostname for the machine.
|
|
1959
|
+
* @param {string} options.ipAddress - The IP address for the machine.
|
|
1960
|
+
* @param {string} options.architecture - The architecture for the machine (default is 'arm64').
|
|
1961
|
+
* @param {string} options.powerType - The power type for the machine (default is 'manual').
|
|
1962
|
+
* @returns {object} An object containing the created machine details.
|
|
1963
|
+
* @memberof UnderpostBaremetal
|
|
1964
|
+
*/
|
|
1965
|
+
machineFactory(
|
|
1966
|
+
options = {
|
|
1967
|
+
macAddress: '',
|
|
1968
|
+
hostname: '',
|
|
1969
|
+
ipAddress: '',
|
|
1970
|
+
architecture: 'arm64',
|
|
1971
|
+
powerType: 'manual',
|
|
1972
|
+
},
|
|
1973
|
+
) {
|
|
1974
|
+
if (!options.powerType) options.powerType = 'manual';
|
|
1975
|
+
const payload = {
|
|
1976
|
+
architecture: options.architecture.match('arm') ? 'arm64/generic' : 'amd64/generic',
|
|
1977
|
+
mac_address: options.macAddress,
|
|
1978
|
+
mac_addresses: options.macAddress,
|
|
1979
|
+
hostname: options.hostname,
|
|
1980
|
+
power_type: options.powerType,
|
|
1981
|
+
ip: options.ipAddress,
|
|
1982
|
+
};
|
|
1983
|
+
logger.info('Creating MAAS machine', payload);
|
|
1984
|
+
const machine = Underpost.baremetal.maasCliExec(
|
|
1985
|
+
`machines create ${Object.keys(payload)
|
|
1986
|
+
.map((k) => `${k}="${payload[k]}"`)
|
|
1987
|
+
.join(' ')}`,
|
|
1988
|
+
);
|
|
1989
|
+
try {
|
|
1990
|
+
return { machine };
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
console.log(error);
|
|
1993
|
+
logger.error(error);
|
|
1994
|
+
throw new Error(`Failed to create MAAS machine. Output:\n${machine}`);
|
|
1995
|
+
}
|
|
1996
|
+
},
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* @method getFamilyBaseOs
|
|
2000
|
+
* @description Determines if the OS belongs to Debian-based or RHEL-based family based on osIdLike string.
|
|
2001
|
+
* @param {string} osIdLike - The os_id_like string from MAAS boot resource or workflow configuration.
|
|
2002
|
+
* @returns {object} An object with boolean properties isDebianBased and isRhelBased indicating the OS family.
|
|
2003
|
+
* @memberof UnderpostBaremetal
|
|
2004
|
+
*/
|
|
2005
|
+
getFamilyBaseOs(osIdLike = '') {
|
|
2006
|
+
return {
|
|
2007
|
+
isDebianBased: osIdLike.match(/debian|ubuntu/i),
|
|
2008
|
+
isRhelBased: osIdLike.match(/rhel|centos|fedora|alma|rocky/i),
|
|
2009
|
+
};
|
|
2010
|
+
},
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* @method kernelFactory
|
|
2014
|
+
* @description Retrieves kernel, initrd, and root filesystem paths from a MAAS boot resource.
|
|
2015
|
+
* @param {object} params - Parameters for the method.
|
|
2016
|
+
* @param {object} params.resource - The MAAS boot resource object.
|
|
2017
|
+
* @param {string} params.type - The type of boot (e.g., 'iso-ram', 'iso-nfs', etc.).
|
|
2018
|
+
* @param {string} params.nfsHostPath - The NFS host path (used for ISO types).
|
|
2019
|
+
* @param {string} params.isoUrl - The ISO URL (used for ISO types).
|
|
2020
|
+
* @param {string} params.workflowId - The workflow identifier.
|
|
2021
|
+
* @returns {object} An object containing paths to the kernel, initrd, and root filesystem.
|
|
2022
|
+
* @memberof UnderpostBaremetal
|
|
2023
|
+
*/
|
|
2024
|
+
kernelFactory({ resource, type, nfsHostPath, isoUrl, workflowId }) {
|
|
2025
|
+
// For disk-based commissioning (casper/iso), use live ISO files
|
|
2026
|
+
if (type === 'iso-ram' || type === 'iso-nfs') {
|
|
2027
|
+
logger.info('Using live ISO for boot (disk-based commissioning)');
|
|
2028
|
+
const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
|
|
2029
|
+
const arch = resource?.architecture
|
|
2030
|
+
? resource.architecture.split('/')[0]
|
|
2031
|
+
: workflowsConfig[workflowId].architecture;
|
|
2032
|
+
const kernelFilesPaths = Underpost.baremetal.downloadISO({
|
|
2033
|
+
resource,
|
|
2034
|
+
architecture: arch,
|
|
2035
|
+
nfsHostPath,
|
|
2036
|
+
isoUrl,
|
|
2037
|
+
osIdLike: workflowsConfig[workflowId].osIdLike || '',
|
|
2038
|
+
});
|
|
2039
|
+
const resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
|
|
2040
|
+
return { kernelFilesPaths, resourcesPath };
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
const resourceData = Underpost.baremetal.maasCliExec(`boot-resource read ${resource.id}`);
|
|
2044
|
+
let kernelFilesPaths = {};
|
|
2045
|
+
const bootFiles = resourceData.sets[Object.keys(resourceData.sets)[0]].files;
|
|
2046
|
+
const arch = resource.architecture.split('/')[0];
|
|
2047
|
+
const resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
|
|
2048
|
+
const kernelPath = `/var/snap/maas/common/maas/image-storage`;
|
|
2049
|
+
|
|
2050
|
+
logger.info('Available boot files', Object.keys(bootFiles));
|
|
2051
|
+
logger.info('Boot files info', {
|
|
2052
|
+
id: resourceData.id,
|
|
2053
|
+
type: resourceData.type,
|
|
2054
|
+
name: resourceData.name,
|
|
2055
|
+
architecture: resourceData.architecture,
|
|
2056
|
+
bootFiles,
|
|
2057
|
+
arch,
|
|
2058
|
+
resourcesPath,
|
|
2059
|
+
kernelPath,
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
// Try standard synced image structure (Ubuntu, CentOS from MAAS repos)
|
|
2063
|
+
const _suffix = resource.architecture.match('xgene') ? '.xgene' : '';
|
|
2064
|
+
if (bootFiles['boot-kernel' + _suffix] && bootFiles['boot-initrd' + _suffix] && bootFiles['squashfs']) {
|
|
2065
|
+
kernelFilesPaths = {
|
|
2066
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel' + _suffix].filename_on_disk}`,
|
|
2067
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd' + _suffix].filename_on_disk}`,
|
|
2068
|
+
squashfs: `${kernelPath}/${bootFiles['squashfs'].filename_on_disk}`,
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
// Try uploaded image structure (Packer-built images, custom uploads)
|
|
2072
|
+
else if (bootFiles['boot-kernel'] && bootFiles['boot-initrd'] && bootFiles['root-tgz']) {
|
|
2073
|
+
kernelFilesPaths = {
|
|
2074
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel'].filename_on_disk}`,
|
|
2075
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd'].filename_on_disk}`,
|
|
2076
|
+
squashfs: `${kernelPath}/${bootFiles['root-tgz'].filename_on_disk}`,
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
// Try alternative uploaded structure with root-image-xz
|
|
2080
|
+
else if (bootFiles['boot-kernel'] && bootFiles['boot-initrd'] && bootFiles['root-image-xz']) {
|
|
2081
|
+
kernelFilesPaths = {
|
|
2082
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel'].filename_on_disk}`,
|
|
2083
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd'].filename_on_disk}`,
|
|
2084
|
+
squashfs: `${kernelPath}/${bootFiles['root-image-xz'].filename_on_disk}`,
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
// Fallback: try to find any kernel, initrd, and root image
|
|
2088
|
+
else {
|
|
2089
|
+
logger.warn('Non-standard boot file structure detected. Available files', Object.keys(bootFiles));
|
|
2090
|
+
|
|
2091
|
+
const rootArchiveKey = Object.keys(bootFiles).find(
|
|
2092
|
+
(k) => k.includes('root') && (k.includes('tgz') || k.includes('tar.gz')),
|
|
2093
|
+
);
|
|
2094
|
+
const explicitKernel = Object.keys(bootFiles).find((k) => k.includes('kernel'));
|
|
2095
|
+
const explicitInitrd = Object.keys(bootFiles).find((k) => k.includes('initrd'));
|
|
2096
|
+
|
|
2097
|
+
if (rootArchiveKey && (!explicitKernel || !explicitInitrd)) {
|
|
2098
|
+
logger.info(`Root archive found (${rootArchiveKey}) and missing kernel/initrd. Attempting to extract.`);
|
|
2099
|
+
const rootArchivePath = `${kernelPath}/${bootFiles[rootArchiveKey].filename_on_disk}`;
|
|
2100
|
+
const tempExtractDir = `/tmp/maas-extract-${resource.id}`;
|
|
2101
|
+
shellExec(`mkdir -p ${tempExtractDir}`);
|
|
2102
|
+
|
|
2103
|
+
// List files in archive to find kernel and initrd
|
|
2104
|
+
const tarList = shellExec(`tar -tf ${rootArchivePath}`).stdout.split('\n');
|
|
2105
|
+
|
|
2106
|
+
// Look for boot/vmlinuz* and boot/initrd* (handling potential leading ./)
|
|
2107
|
+
// Skip rescue, kdump, and other special images
|
|
2108
|
+
const vmlinuzPaths = tarList.filter(
|
|
2109
|
+
(f) => f.match(/(\.\/)?boot\/vmlinuz-[0-9]/) && !f.includes('rescue') && !f.includes('kdump'),
|
|
2110
|
+
);
|
|
2111
|
+
const initrdPaths = tarList.filter(
|
|
2112
|
+
(f) =>
|
|
2113
|
+
f.match(/(\.\/)?boot\/(initrd|initramfs)-[0-9]/) &&
|
|
2114
|
+
!f.includes('rescue') &&
|
|
2115
|
+
!f.includes('kdump') &&
|
|
2116
|
+
f.includes('.img'),
|
|
2117
|
+
);
|
|
2118
|
+
|
|
2119
|
+
logger.info(`Found kernel candidates:`, { vmlinuzPaths, initrdPaths });
|
|
2120
|
+
|
|
2121
|
+
// Try to match kernel and initrd by version number
|
|
2122
|
+
let vmlinuzPath = null;
|
|
2123
|
+
let initrdPath = null;
|
|
2124
|
+
|
|
2125
|
+
if (vmlinuzPaths.length > 0 && initrdPaths.length > 0) {
|
|
2126
|
+
// Extract version from kernel filename (e.g., "5.14.0-611.11.1.el9_7.aarch64")
|
|
2127
|
+
for (const kernelPath of vmlinuzPaths.sort().reverse()) {
|
|
2128
|
+
const kernelVersion = kernelPath.match(/vmlinuz-(.+)$/)?.[1];
|
|
2129
|
+
if (kernelVersion) {
|
|
2130
|
+
// Look for matching initrd
|
|
2131
|
+
const matchingInitrd = initrdPaths.find((p) => p.includes(kernelVersion));
|
|
2132
|
+
if (matchingInitrd) {
|
|
2133
|
+
vmlinuzPath = kernelPath;
|
|
2134
|
+
initrdPath = matchingInitrd;
|
|
2135
|
+
logger.info(`Matched kernel and initrd by version: ${kernelVersion}`);
|
|
2136
|
+
break;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// Fallback: use newest versions if no match found
|
|
2143
|
+
if (!vmlinuzPath && vmlinuzPaths.length > 0) {
|
|
2144
|
+
vmlinuzPath = vmlinuzPaths.sort().pop();
|
|
2145
|
+
}
|
|
2146
|
+
if (!initrdPath && initrdPaths.length > 0) {
|
|
2147
|
+
initrdPath = initrdPaths.sort().pop();
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
logger.info(`Selected kernel: ${vmlinuzPath}, initrd: ${initrdPath}`);
|
|
2151
|
+
|
|
2152
|
+
if (vmlinuzPath && initrdPath) {
|
|
2153
|
+
// Extract specific files
|
|
2154
|
+
// Extract all files in boot/ to ensure symlinks resolve
|
|
2155
|
+
shellExec(`tar -xf ${rootArchivePath} -C ${tempExtractDir} --wildcards '*boot/*'`);
|
|
2156
|
+
|
|
2157
|
+
kernelFilesPaths = {
|
|
2158
|
+
'vmlinuz-efi': `${tempExtractDir}/${vmlinuzPath}`,
|
|
2159
|
+
'initrd.img': `${tempExtractDir}/${initrdPath}`,
|
|
2160
|
+
squashfs: rootArchivePath,
|
|
2161
|
+
};
|
|
2162
|
+
logger.info('Extracted kernel and initrd from root archive.');
|
|
2163
|
+
} else {
|
|
2164
|
+
logger.error(
|
|
2165
|
+
`Failed to find kernel/initrd in archive. Contents of boot/ directory:`,
|
|
2166
|
+
tarList.filter((f) => f.includes('boot/')),
|
|
2167
|
+
);
|
|
2168
|
+
throw new Error(`Could not find kernel or initrd in ${rootArchiveKey}`);
|
|
2169
|
+
}
|
|
2170
|
+
} else {
|
|
2171
|
+
const kernelFile = Object.keys(bootFiles).find((k) => k.includes('kernel')) || Object.keys(bootFiles)[0];
|
|
2172
|
+
const initrdFile = Object.keys(bootFiles).find((k) => k.includes('initrd')) || Object.keys(bootFiles)[1];
|
|
2173
|
+
const rootFile =
|
|
2174
|
+
Object.keys(bootFiles).find(
|
|
2175
|
+
(k) => k.includes('root') || k.includes('squashfs') || k.includes('tgz') || k.includes('xz'),
|
|
2176
|
+
) || Object.keys(bootFiles)[2];
|
|
2177
|
+
|
|
2178
|
+
if (kernelFile && initrdFile && rootFile) {
|
|
2179
|
+
kernelFilesPaths = {
|
|
2180
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles[kernelFile].filename_on_disk}`,
|
|
2181
|
+
'initrd.img': `${kernelPath}/${bootFiles[initrdFile].filename_on_disk}`,
|
|
2182
|
+
squashfs: `${kernelPath}/${bootFiles[rootFile].filename_on_disk}`,
|
|
2183
|
+
};
|
|
2184
|
+
logger.info('Using detected files', { kernel: kernelFile, initrd: initrdFile, root: rootFile });
|
|
2185
|
+
} else {
|
|
2186
|
+
throw new Error(`Cannot identify boot files. Available: ${Object.keys(bootFiles).join(', ')}`);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
resource,
|
|
2192
|
+
bootFiles,
|
|
2193
|
+
arch,
|
|
2194
|
+
resourcesPath,
|
|
2195
|
+
kernelPath,
|
|
2196
|
+
resourceData,
|
|
2197
|
+
kernelFilesPaths,
|
|
2198
|
+
};
|
|
2199
|
+
},
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* @method writeGrubConfigToFile
|
|
2203
|
+
* @description Writes the GRUB configuration content to the grub.cfg file in the TFTP root.
|
|
2204
|
+
* @param {object} params - Parameters for the method.
|
|
2205
|
+
* @param {string} params.grubCfgSrc - The GRUB configuration content to write.
|
|
2206
|
+
* @memberof UnderpostBaremetal
|
|
2207
|
+
* @returns {void}
|
|
2208
|
+
*/
|
|
2209
|
+
writeGrubConfigToFile({ grubCfgSrc = '' }) {
|
|
2210
|
+
shellExec(`mkdir -p ${process.env.TFTP_ROOT}/grub`, {
|
|
2211
|
+
disableLog: true,
|
|
2212
|
+
});
|
|
2213
|
+
return fs.writeFileSync(`${process.env.TFTP_ROOT}/grub/grub.cfg`, grubCfgSrc, 'utf8');
|
|
2214
|
+
},
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* @method getGrubConfigFromFile
|
|
2218
|
+
* @description Reads the GRUB configuration content from the grub.cfg file in the TFTP root.
|
|
2219
|
+
* @memberof UnderpostBaremetal
|
|
2220
|
+
* @returns {string} The GRUB configuration content.
|
|
2221
|
+
*/
|
|
2222
|
+
getGrubConfigFromFile() {
|
|
2223
|
+
const grubCfgPath = `${process.env.TFTP_ROOT}/grub/grub.cfg`;
|
|
2224
|
+
const grubCfgSrc = fs.readFileSync(grubCfgPath, 'utf8');
|
|
2225
|
+
return { grubCfgPath, grubCfgSrc };
|
|
2226
|
+
},
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* @method removeDiscoveredMachines
|
|
2230
|
+
* @description Removes all machines in the 'discovered' status from MAAS.
|
|
2231
|
+
* @memberof UnderpostBaremetal
|
|
2232
|
+
* @returns {void}
|
|
2233
|
+
*/
|
|
2234
|
+
removeDiscoveredMachines() {
|
|
2235
|
+
logger.info('Removing all discovered machines from MAAS...');
|
|
2236
|
+
Underpost.baremetal.maasCliExec(`discoveries clear all=true`);
|
|
2237
|
+
},
|
|
2238
|
+
|
|
2239
|
+
/**
|
|
2240
|
+
* @method efiGrubModulesFactory
|
|
2241
|
+
* @description Copies the appropriate EFI GRUB modules to the TFTP root based on the image architecture.
|
|
2242
|
+
* @param {object} options - Options for determining which GRUB modules to copy.
|
|
2243
|
+
* @param {object} options.image - Image configuration object.
|
|
2244
|
+
* @param {string} options.image.architecture - The architecture of the image ('amd64' or 'arm64').
|
|
2245
|
+
* @memberof UnderpostBaremetal
|
|
2246
|
+
* @returns {void}
|
|
2247
|
+
*/
|
|
2248
|
+
efiGrubModulesFactory(options = { image: { architecture: 'amd64' } }) {
|
|
2249
|
+
if (options.image.architecture.match('arm64')) {
|
|
2250
|
+
// Copy ARM64 EFI GRUB modules.
|
|
2251
|
+
const arm64EfiPath = `${process.env.TFTP_ROOT}/grub/arm64-efi`;
|
|
2252
|
+
if (fs.existsSync(arm64EfiPath)) shellExec(`sudo rm -rf ${arm64EfiPath}`);
|
|
2253
|
+
shellExec(`sudo cp -a /usr/lib/grub/arm64-efi ${arm64EfiPath}`);
|
|
2254
|
+
} else {
|
|
2255
|
+
// Copy AMD64 EFI GRUB modules.
|
|
2256
|
+
const amd64EfiPath = `${process.env.TFTP_ROOT}/grub/x86_64-efi`;
|
|
2257
|
+
if (fs.existsSync(amd64EfiPath)) shellExec(`sudo rm -rf ${amd64EfiPath}`);
|
|
2258
|
+
shellExec(`sudo cp -a /usr/lib/grub/x86_64-efi ${amd64EfiPath}`);
|
|
2259
|
+
}
|
|
2260
|
+
},
|
|
2261
|
+
|
|
2262
|
+
/**
|
|
2263
|
+
* @method ipxeEmbeddedScriptFactory
|
|
2264
|
+
* @description Generates the embedded iPXE boot script that performs DHCP and chains to the main script.
|
|
2265
|
+
* This script is embedded into the iPXE binary or loaded first by GRUB.
|
|
2266
|
+
* Supports MAC address spoofing for baremetal commissioning workflows.
|
|
2267
|
+
* @param {object} params - The parameters for generating the script.
|
|
2268
|
+
* @param {string} params.tftpServer - The IP address of the TFTP server.
|
|
2269
|
+
* @param {string} params.scriptPath - The path to the main iPXE script on TFTP server.
|
|
2270
|
+
* @param {string} [params.macAddress] - Optional MAC address to spoof for MAAS registration.
|
|
2271
|
+
* @returns {string} The embedded iPXE script content.
|
|
2272
|
+
* @memberof UnderpostBaremetal
|
|
2273
|
+
*/
|
|
2274
|
+
ipxeEmbeddedScriptFactory({ tftpServer, scriptPath, macAddress = null }) {
|
|
2275
|
+
const macSpoofingBlock =
|
|
2276
|
+
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
2277
|
+
? `
|
|
2278
|
+
# MAC Address Information
|
|
2279
|
+
echo ========================================
|
|
2280
|
+
echo Target MAC for MAAS: ${macAddress}
|
|
2281
|
+
echo Hardware MAC: \${net0/mac}
|
|
2282
|
+
echo ========================================
|
|
2283
|
+
echo NOTE: iPXE MAC spoofing does NOT persist to kernel
|
|
2284
|
+
echo The kernel will receive MAC via ifname= parameter
|
|
2285
|
+
echo ========================================
|
|
2286
|
+
`
|
|
2287
|
+
: `
|
|
2288
|
+
# Using hardware MAC address
|
|
2289
|
+
echo ========================================
|
|
2290
|
+
echo Using device hardware MAC address
|
|
2291
|
+
echo Hardware MAC: \${net0/mac}
|
|
2292
|
+
echo MAC spoofing disabled - device uses actual MAC
|
|
2293
|
+
echo ========================================
|
|
2294
|
+
`;
|
|
2295
|
+
|
|
2296
|
+
return `#!ipxe
|
|
2297
|
+
# Embedded iPXE Boot Script
|
|
2298
|
+
# This script performs DHCP configuration and chains to the main boot script
|
|
2299
|
+
|
|
2300
|
+
echo ========================================
|
|
2301
|
+
echo iPXE Embedded Boot Loader
|
|
2302
|
+
echo ========================================
|
|
2303
|
+
echo TFTP Server: ${tftpServer}
|
|
2304
|
+
echo Script Path: ${scriptPath}
|
|
2305
|
+
echo ========================================
|
|
2306
|
+
|
|
2307
|
+
${macSpoofingBlock}
|
|
2308
|
+
|
|
2309
|
+
# Show network interface info before DHCP
|
|
2310
|
+
echo Network Interface Info (before DHCP):
|
|
2311
|
+
echo Interface: net0
|
|
2312
|
+
echo MAC: \${net0/mac}
|
|
2313
|
+
ifstat
|
|
2314
|
+
|
|
2315
|
+
# Perform DHCP to get network configuration
|
|
2316
|
+
echo ========================================
|
|
2317
|
+
echo Performing DHCP configuration...
|
|
2318
|
+
echo ========================================
|
|
2319
|
+
dhcp net0 || goto dhcp_retry
|
|
2320
|
+
|
|
2321
|
+
echo DHCP configuration successful
|
|
2322
|
+
echo IP Address: \${net0/ip}
|
|
2323
|
+
echo Netmask: \${net0/netmask}
|
|
2324
|
+
echo Gateway: \${net0/gateway}
|
|
2325
|
+
echo DNS: \${net0/dns}
|
|
2326
|
+
echo TFTP Server: \${next-server}
|
|
2327
|
+
echo MAC used by DHCP: \${net0/mac}
|
|
2328
|
+
|
|
2329
|
+
# Chain to the main iPXE script
|
|
2330
|
+
echo ========================================
|
|
2331
|
+
echo Chainloading main boot script...
|
|
2332
|
+
echo Script: tftp://${tftpServer}${scriptPath}
|
|
2333
|
+
echo ========================================
|
|
2334
|
+
chain tftp://${tftpServer}${scriptPath} || goto chain_failed
|
|
2335
|
+
|
|
2336
|
+
:dhcp_retry
|
|
2337
|
+
echo DHCP failed, retrying in 5 seconds...
|
|
2338
|
+
sleep 5
|
|
2339
|
+
dhcp net0 || goto dhcp_retry
|
|
2340
|
+
goto dhcp_success
|
|
2341
|
+
|
|
2342
|
+
:dhcp_success
|
|
2343
|
+
echo DHCP retry successful
|
|
2344
|
+
echo IP Address: \${net0/ip}
|
|
2345
|
+
echo MAC: \${net0/mac}
|
|
2346
|
+
chain tftp://${tftpServer}${scriptPath} || goto chain_failed
|
|
2347
|
+
|
|
2348
|
+
:chain_failed
|
|
2349
|
+
echo ========================================
|
|
2350
|
+
echo ERROR: Failed to chain to main script
|
|
2351
|
+
echo TFTP Server: ${tftpServer}
|
|
2352
|
+
echo Script Path: ${scriptPath}
|
|
2353
|
+
echo ========================================
|
|
2354
|
+
echo Retrying in 10 seconds...
|
|
2355
|
+
sleep 10
|
|
2356
|
+
chain tftp://${tftpServer}${scriptPath} || goto shell_debug
|
|
2357
|
+
|
|
2358
|
+
:shell_debug
|
|
2359
|
+
echo Dropping to iPXE shell for manual debugging
|
|
2360
|
+
echo Try: chain tftp://${tftpServer}${scriptPath}
|
|
2361
|
+
shell
|
|
2362
|
+
`;
|
|
2363
|
+
},
|
|
2364
|
+
|
|
2365
|
+
/**
|
|
2366
|
+
* @method ipxeScriptFactory
|
|
2367
|
+
* @description Generates the iPXE script content for stable identity.
|
|
2368
|
+
* This iPXE script uses directly boots kernel/initrd via TFTP.
|
|
2369
|
+
* When minimal mode is enabled, generates a simple dhcp+kernel+initrd+boot script.
|
|
2370
|
+
* @param {object} params - The parameters for generating the script.
|
|
2371
|
+
* @param {string} params.maasIp - The IP address of the MAAS/file server.
|
|
2372
|
+
* @param {string} [params.macAddress] - The MAC address registered in MAAS (for display only).
|
|
2373
|
+
* @param {string} [params.architecture] - The architecture (arm64/amd64).
|
|
2374
|
+
* @param {string} params.tftpPrefix - The TFTP prefix path (e.g., 'rpi4mb').
|
|
2375
|
+
* @param {string} params.kernelCmd - The kernel command line parameters.
|
|
2376
|
+
* @param {boolean} [params.minimal=false] - Generate a minimal embedded script for ISO builds.
|
|
2377
|
+
* @returns {string} The iPXE script content.
|
|
2378
|
+
* @memberof UnderpostBaremetal
|
|
2379
|
+
*/
|
|
2380
|
+
ipxeScriptFactory({ maasIp, macAddress, architecture, tftpPrefix, kernelCmd, minimal = false }) {
|
|
2381
|
+
if (minimal) {
|
|
2382
|
+
return `#!ipxe
|
|
2383
|
+
dhcp
|
|
2384
|
+
set server_ip ${maasIp}
|
|
2385
|
+
set tftp_prefix ${tftpPrefix}
|
|
2386
|
+
kernel tftp://\${server_ip}/\${tftp_prefix}/pxe/vmlinuz-efi ${kernelCmd}
|
|
2387
|
+
initrd tftp://\${server_ip}/\${tftp_prefix}/pxe/initrd.img
|
|
2388
|
+
boot || shell
|
|
2389
|
+
`;
|
|
2390
|
+
}
|
|
2391
|
+
const macInfo =
|
|
2392
|
+
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
2393
|
+
? `echo Registered MAC: ${macAddress}`
|
|
2394
|
+
: `echo Using hardware MAC address`;
|
|
2395
|
+
|
|
2396
|
+
// Construct the full TFTP paths for kernel and initrd
|
|
2397
|
+
const kernelPath = `${tftpPrefix}/pxe/vmlinuz-efi`;
|
|
2398
|
+
const initrdPath = `${tftpPrefix}/pxe/initrd.img`;
|
|
2399
|
+
const grubBootloader = architecture === 'arm64' ? 'grubaa64.efi' : 'grubx64.efi';
|
|
2400
|
+
const grubPath = `${tftpPrefix}/pxe/${grubBootloader}`;
|
|
2401
|
+
|
|
2402
|
+
return `#!ipxe
|
|
2403
|
+
echo ========================================
|
|
2404
|
+
echo iPXE Network Boot
|
|
2405
|
+
echo ========================================
|
|
2406
|
+
echo MAAS Server: ${maasIp}
|
|
2407
|
+
echo Architecture: ${architecture}
|
|
2408
|
+
${macInfo}
|
|
2409
|
+
echo ========================================
|
|
2410
|
+
|
|
2411
|
+
# Show current network configuration
|
|
2412
|
+
echo Current Network Configuration:
|
|
2413
|
+
ifstat
|
|
2414
|
+
|
|
2415
|
+
# Display MAC address information
|
|
2416
|
+
echo MAC Address: \${net0/mac}
|
|
2417
|
+
echo IP Address: \${net0/ip}
|
|
2418
|
+
echo Gateway: \${net0/gateway}
|
|
2419
|
+
echo DNS: \${net0/dns}
|
|
2420
|
+
${macAddress && macAddress !== '00:00:00:00:00:00' ? `echo Target MAC for kernel: ${macAddress}` : ''}
|
|
2421
|
+
|
|
2422
|
+
# Direct kernel/initrd boot via TFTP
|
|
2423
|
+
# Modern simplified approach: Direct kernel/initrd boot via TFTP
|
|
2424
|
+
echo ========================================
|
|
2425
|
+
echo Loading kernel and initrd via TFTP...
|
|
2426
|
+
echo Kernel: tftp://${maasIp}/${kernelPath}
|
|
2427
|
+
echo Initrd: tftp://${maasIp}/${initrdPath}
|
|
2428
|
+
${
|
|
2429
|
+
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
2430
|
+
? `echo Kernel will use MAC: ${macAddress} (via ifname parameter)`
|
|
2431
|
+
: 'echo Kernel will use hardware MAC'
|
|
2432
|
+
}
|
|
2433
|
+
echo ========================================
|
|
2434
|
+
|
|
2435
|
+
# Load kernel via TFTP
|
|
2436
|
+
kernel tftp://${maasIp}/${kernelPath} ${kernelCmd || 'console=ttyS0,115200'} || goto grub_fallback
|
|
2437
|
+
echo Kernel loaded successfully
|
|
2438
|
+
|
|
2439
|
+
# Load initrd via TFTP
|
|
2440
|
+
initrd tftp://${maasIp}/${initrdPath} || goto grub_fallback
|
|
2441
|
+
echo Initrd loaded successfully
|
|
2442
|
+
|
|
2443
|
+
# Boot the kernel
|
|
2444
|
+
echo Booting kernel...
|
|
2445
|
+
boot
|
|
2446
|
+
|
|
2447
|
+
:grub_fallback
|
|
2448
|
+
echo ========================================
|
|
2449
|
+
echo Direct kernel boot failed, falling back to GRUB chainload...
|
|
2450
|
+
echo TFTP Path: tftp://${maasIp}/${grubPath}
|
|
2451
|
+
echo ========================================
|
|
2452
|
+
|
|
2453
|
+
# Fallback: Chain to GRUB via TFTP (avoids malformed HTTP bootloader issues)
|
|
2454
|
+
chain tftp://${maasIp}/${grubPath} || goto http_fallback
|
|
2455
|
+
|
|
2456
|
+
:http_fallback
|
|
2457
|
+
echo TFTP GRUB chainload failed, trying HTTP fallback...
|
|
2458
|
+
echo ========================================
|
|
2459
|
+
|
|
2460
|
+
# Fallback: Try MAAS HTTP bootloader (may have certificate issues)
|
|
2461
|
+
set boot-url http://${maasIp}:5248/images/bootloader
|
|
2462
|
+
echo Boot URL: \${boot-url}
|
|
2463
|
+
chain \${boot-url}/uefi/${architecture}/${
|
|
2464
|
+
grubBootloader === 'grubaa64.efi' ? 'bootaa64.efi' : 'bootx64.efi'
|
|
2465
|
+
} || goto error
|
|
2466
|
+
|
|
2467
|
+
:error
|
|
2468
|
+
echo ========================================
|
|
2469
|
+
echo ERROR: All boot methods failed
|
|
2470
|
+
echo ========================================
|
|
2471
|
+
echo MAAS IP: ${maasIp}
|
|
2472
|
+
echo Architecture: ${architecture}
|
|
2473
|
+
echo MAC: \${net0/mac}
|
|
2474
|
+
echo IP: \${net0/ip}
|
|
2475
|
+
echo ========================================
|
|
2476
|
+
echo Retrying GRUB TFTP in 10 seconds...
|
|
2477
|
+
sleep 10
|
|
2478
|
+
chain tftp://${maasIp}/${grubPath} || goto shell_debug
|
|
2479
|
+
|
|
2480
|
+
:shell_debug
|
|
2481
|
+
echo Dropping to iPXE shell for manual intervention
|
|
2482
|
+
shell
|
|
2483
|
+
`;
|
|
2484
|
+
},
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* @method ipxeEfiFactory
|
|
2488
|
+
* @description Manages iPXE EFI binary build with cache support.
|
|
2489
|
+
* Checks cache, builds only if needed, saves to cache after build.
|
|
2490
|
+
* @param {object} params - The parameters for iPXE build.
|
|
2491
|
+
* @param {string} params.tftpRootPath - TFTP root directory path.
|
|
2492
|
+
* @param {string} params.ipxeCacheDir - iPXE cache directory path.
|
|
2493
|
+
* @param {string} params.arch - Target architecture (arm64/amd64).
|
|
2494
|
+
* @param {string} params.underpostRoot - Underpost root directory.
|
|
2495
|
+
* @param {string} [params.embeddedScriptPath] - Path to embedded boot script.
|
|
2496
|
+
* @param {boolean} [params.forceRebuild=false] - Force rebuild regardless of cache.
|
|
2497
|
+
* @returns {void}
|
|
2498
|
+
* @memberof UnderpostBaremetal
|
|
2499
|
+
*/
|
|
2500
|
+
ipxeEfiFactory({ tftpRootPath, ipxeCacheDir, arch, underpostRoot, embeddedScriptPath, forceRebuild = false }) {
|
|
2501
|
+
const embedArg =
|
|
2502
|
+
embeddedScriptPath && fs.existsSync(embeddedScriptPath) ? ` --embed-script ${embeddedScriptPath}` : '';
|
|
2503
|
+
const rebuildArg = forceRebuild ? ' --rebuild' : '';
|
|
2504
|
+
|
|
2505
|
+
logger.info('Building iPXE EFI binary...', { embeddedScriptPath, forced: forceRebuild });
|
|
2506
|
+
shellExec(`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch}${embedArg}${rebuildArg}`);
|
|
2507
|
+
|
|
2508
|
+
shellExec(`mkdir -p ${ipxeCacheDir}`);
|
|
2509
|
+
shellExec(`cp ${tftpRootPath}/ipxe.efi ${ipxeCacheDir}/ipxe.efi`);
|
|
2510
|
+
},
|
|
2511
|
+
|
|
2512
|
+
/**
|
|
2513
|
+
* @method grubFactory
|
|
2514
|
+
* @description Generates the GRUB configuration file content.
|
|
2515
|
+
* @param {object} params - The parameters for generating the configuration.
|
|
2516
|
+
* @param {string} params.menuentryStr - The title of the menu entry.
|
|
2517
|
+
* @param {string} params.kernelPath - The path to the kernel file (relative to TFTP root).
|
|
2518
|
+
* @param {string} params.initrdPath - The path to the initrd file (relative to TFTP root).
|
|
2519
|
+
* @param {string} params.cmd - The kernel command line parameters.
|
|
2520
|
+
* @param {string} params.tftpIp - The IP address of the TFTP server.
|
|
2521
|
+
* @param {boolean} [params.ipxe] - Flag to enable iPXE chainloading.
|
|
2522
|
+
* @param {string} [params.ipxePath] - The path to the iPXE binary.
|
|
2523
|
+
* @returns {object} An object containing 'grubCfgSrc' the GRUB configuration source string.
|
|
2524
|
+
* @memberof UnderpostBaremetal
|
|
2525
|
+
*/
|
|
2526
|
+
grubFactory({ menuentryStr, kernelPath, initrdPath, cmd, tftpIp, ipxe, ipxePath }) {
|
|
2527
|
+
if (ipxe) {
|
|
2528
|
+
return {
|
|
2529
|
+
grubCfgSrc: `
|
|
2530
|
+
set default="0"
|
|
2531
|
+
set timeout=10
|
|
2532
|
+
insmod tftp
|
|
2533
|
+
set root=(tftp,${tftpIp})
|
|
2534
|
+
|
|
2535
|
+
menuentry 'iPXE ${menuentryStr}' {
|
|
2536
|
+
echo "Loading iPXE with embedded script..."
|
|
2537
|
+
echo "[INFO] TFTP Server: ${tftpIp}"
|
|
2538
|
+
echo "[INFO] iPXE Binary: ${ipxePath}"
|
|
2539
|
+
echo "[INFO] iPXE will execute embedded script (dhcp + chain)"
|
|
2540
|
+
chainloader ${ipxePath}
|
|
2541
|
+
boot
|
|
2542
|
+
}
|
|
2543
|
+
`,
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
return {
|
|
2547
|
+
grubCfgSrc: `
|
|
2548
|
+
set default="0"
|
|
2549
|
+
set timeout=10
|
|
2550
|
+
insmod nfs
|
|
2551
|
+
insmod gzio
|
|
2552
|
+
insmod http
|
|
2553
|
+
insmod tftp
|
|
2554
|
+
set root=(tftp,${tftpIp})
|
|
2555
|
+
|
|
2556
|
+
menuentry '${menuentryStr}' {
|
|
2557
|
+
echo "${menuentryStr}"
|
|
2558
|
+
echo " ${Underpost.version}"
|
|
2559
|
+
echo "Date: ${new Date().toISOString()}"
|
|
2560
|
+
${
|
|
2561
|
+
cmd.match('/MAAS/metadata/by-id/')
|
|
2562
|
+
? `echo "System ID: ${cmd.split('/MAAS/metadata/by-id/')[1].split('/')[0]}"`
|
|
2563
|
+
: ''
|
|
2564
|
+
}
|
|
2565
|
+
echo "TFTP server: ${tftpIp}"
|
|
2566
|
+
echo "Kernel path: ${kernelPath}"
|
|
2567
|
+
echo "Initrd path: ${initrdPath}"
|
|
2568
|
+
echo "Starting boot process..."
|
|
2569
|
+
echo "Loading kernel..."
|
|
2570
|
+
linux /${kernelPath} ${cmd}
|
|
2571
|
+
echo "Loading initrd..."
|
|
2572
|
+
initrd /${initrdPath}
|
|
2573
|
+
echo "Booting..."
|
|
2574
|
+
boot
|
|
2575
|
+
}
|
|
2576
|
+
`,
|
|
2577
|
+
};
|
|
2578
|
+
},
|
|
2579
|
+
|
|
2580
|
+
/**
|
|
2581
|
+
* @method bootstrapHttpServerPortFactory
|
|
2582
|
+
* @description Determines the bootstrap HTTP server port.
|
|
2583
|
+
* @param {object} params - Parameters for determining the port.
|
|
2584
|
+
* @param {number} [params.port] - The port passed via options.
|
|
2585
|
+
* @param {string} params.workflowId - The workflow identifier.
|
|
2586
|
+
* @param {object} params.workflowsConfig - The loaded workflows configuration.
|
|
2587
|
+
* @returns {number} The determined port number.
|
|
2588
|
+
* @memberof UnderpostBaremetal
|
|
2589
|
+
*/
|
|
2590
|
+
bootstrapHttpServerPortFactory({ port, workflowId, workflowsConfig }) {
|
|
2591
|
+
return port || workflowsConfig[workflowId]?.bootstrapHttpServerPort || 8888;
|
|
2592
|
+
},
|
|
2593
|
+
|
|
2594
|
+
/**
|
|
2595
|
+
* @method resolveSshKeyPaths
|
|
2596
|
+
* @description Resolves the SSH key pair and login user for commissioning/
|
|
2597
|
+
* orchestration. The key pair always lives at `<dir>/id_rsa` + `<dir>/id_rsa.pub`.
|
|
2598
|
+
*
|
|
2599
|
+
* Key-dir precedence:
|
|
2600
|
+
* 1. `--ssh-key-dir <dir>` (explicit path)
|
|
2601
|
+
* 2. `--deploy-id <id>` (+ `--user <user>`) → `engine-private/conf/<id>/users/<user>`
|
|
2602
|
+
* (the same user↔deployId↔key convention `src/cli/ssh.js` writes; no key path needed)
|
|
2603
|
+
* 3. workflow `sshKeyDir`
|
|
2604
|
+
* 4. workflow `deployId` (+ `user`)
|
|
2605
|
+
* 5. default `engine-private/deploy`
|
|
2606
|
+
*
|
|
2607
|
+
* User precedence: `--user` > workflow `user` > `root`. A leading `~` in the
|
|
2608
|
+
* resolved dir is expanded to the user's home.
|
|
2609
|
+
* @param {object} params
|
|
2610
|
+
* @param {object} [params.options={}] - CLI options (sshKeyDir, deployId, user).
|
|
2611
|
+
* @param {object} [params.workflowsConfig={}] - Loaded workflows config.
|
|
2612
|
+
* @param {string} [params.workflowId=''] - Active workflow id.
|
|
2613
|
+
* @returns {{ keyDir: string, privateKeyPath: string, publicKeyPath: string, user: string, deployId: string }}
|
|
2614
|
+
* @memberof UnderpostBaremetal
|
|
2615
|
+
*/
|
|
2616
|
+
resolveSshKeyPaths({ options = {}, workflowsConfig = {}, workflowId = '' } = {}) {
|
|
2617
|
+
const wf = workflowsConfig?.[workflowId] || {};
|
|
2618
|
+
const user = options.user || wf.user || 'root';
|
|
2619
|
+
const deployId = options.deployId || wf.deployId || '';
|
|
2620
|
+
|
|
2621
|
+
let dir;
|
|
2622
|
+
if (options.sshKeyDir) dir = options.sshKeyDir;
|
|
2623
|
+
else if (options.deployId) dir = `engine-private/conf/${options.deployId}/users/${user}`;
|
|
2624
|
+
else if (wf.sshKeyDir) dir = wf.sshKeyDir;
|
|
2625
|
+
else if (deployId) dir = `engine-private/conf/${deployId}/users/${user}`;
|
|
2626
|
+
else dir = 'engine-private/deploy';
|
|
2627
|
+
|
|
2628
|
+
if (dir.startsWith('~')) dir = path.join(process.env.HOME || '', dir.slice(1));
|
|
2629
|
+
return { keyDir: dir, privateKeyPath: `${dir}/id_rsa`, publicKeyPath: `${dir}/id_rsa.pub`, user, deployId };
|
|
2630
|
+
},
|
|
2631
|
+
|
|
2632
|
+
/**
|
|
2633
|
+
* @method resolveInstallCredentials
|
|
2634
|
+
* @description Resolves the login accounts baked into the deployed OS. Always
|
|
2635
|
+
* creates TWO admin accounts so a console login is guaranteed:
|
|
2636
|
+
* 1. the MAAS admin (MAAS_ADMIN_USERNAME / MAAS_ADMIN_PASS) — password login,
|
|
2637
|
+
* 2. the deploy user (`--user`, e.g. admin) — its conf.node.json
|
|
2638
|
+
* password (or MAAS_ADMIN_PASS), plus the SSH key.
|
|
2639
|
+
* root always gets MAAS_ADMIN_PASS. The deploy user/password come from
|
|
2640
|
+
* `engine-private/conf/<deployId>/conf.node.json` (`users[user]`).
|
|
2641
|
+
* @param {object} params
|
|
2642
|
+
* @param {object} [params.options={}] - CLI options.
|
|
2643
|
+
* @param {object} [params.workflowsConfig={}] - Loaded workflows config.
|
|
2644
|
+
* @param {string} [params.workflowId=''] - Active workflow id.
|
|
2645
|
+
* @returns {{ rootPassword: string, adminUsername: string, adminPassword: string, deployUsername: string, deployPassword: string, confUser: object }}
|
|
2646
|
+
* @memberof UnderpostBaremetal
|
|
2647
|
+
*/
|
|
2648
|
+
resolveInstallCredentials({ options = {}, workflowsConfig = {}, workflowId = '' } = {}) {
|
|
2649
|
+
const { user, deployId } = Underpost.baremetal.resolveSshKeyPaths({ options, workflowsConfig, workflowId });
|
|
2650
|
+
|
|
2651
|
+
let confUser = {};
|
|
2652
|
+
if (deployId) {
|
|
2653
|
+
const confPath = `engine-private/conf/${deployId}/conf.node.json`;
|
|
2654
|
+
if (fs.existsSync(confPath)) {
|
|
2655
|
+
try {
|
|
2656
|
+
confUser = JSON.parse(fs.readFileSync(confPath, 'utf8'))?.users?.[user] || {};
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
logger.warn(`Failed to parse ${confPath}: ${error.message}`);
|
|
2659
|
+
}
|
|
2660
|
+
} else {
|
|
2661
|
+
logger.warn(`conf.node.json not found at ${confPath}; using MAAS_ADMIN_* defaults`);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
const maasPass = process.env.MAAS_ADMIN_PASS || '';
|
|
2666
|
+
const rootPassword = maasPass;
|
|
2667
|
+
// User 1: the MAAS admin — guaranteed password login on the console.
|
|
2668
|
+
const adminUsername = process.env.MAAS_ADMIN_USERNAME || 'maas';
|
|
2669
|
+
const adminPassword = maasPass;
|
|
2670
|
+
// User 2: the deploy user (only when distinct from root/maas admin).
|
|
2671
|
+
const deployUsername = deployId && user && user !== 'root' && user !== adminUsername ? user : '';
|
|
2672
|
+
const deployPassword = deployUsername ? confUser.password || maasPass : '';
|
|
2673
|
+
|
|
2674
|
+
if (!rootPassword) {
|
|
2675
|
+
logger.warn(
|
|
2676
|
+
'MAAS_ADMIN_PASS is empty — console password login will not work. Set MAAS_ADMIN_PASS in .env so root/admin have a usable password.',
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
return { rootPassword, adminUsername, adminPassword, deployUsername, deployPassword, confUser };
|
|
2680
|
+
},
|
|
2681
|
+
|
|
2682
|
+
/**
|
|
2683
|
+
* @method readEngineConfig
|
|
2684
|
+
* @description Reads an engine config/secret value (e.g. GITHUB_TOKEN) on the
|
|
2685
|
+
* controller. Prefers the loaded process env, falling back to
|
|
2686
|
+
* `node bin config get --plain <key>`.
|
|
2687
|
+
* @param {string} key - Config/env key name.
|
|
2688
|
+
* @returns {string} The resolved value, or '' if unset.
|
|
2689
|
+
* @memberof UnderpostBaremetal
|
|
2690
|
+
*/
|
|
2691
|
+
readEngineConfig(key) {
|
|
2692
|
+
if (process.env[key]) return process.env[key];
|
|
2693
|
+
const out = shellExec(`node bin config get --plain ${key}`, {
|
|
2694
|
+
stdout: true,
|
|
2695
|
+
silent: true,
|
|
2696
|
+
silentOnError: true,
|
|
2697
|
+
disableLog: true,
|
|
2698
|
+
});
|
|
2699
|
+
const value = `${out || ''}`.trim();
|
|
2700
|
+
return value && value !== 'undefined' ? value : '';
|
|
2701
|
+
},
|
|
2702
|
+
|
|
2703
|
+
/**
|
|
2704
|
+
* @method resolveInstalledNetwork
|
|
2705
|
+
* @description Resolves the static network config written into the deployed OS
|
|
2706
|
+
* so the controller can reach it at a known IP after reboot. Uses the same IP
|
|
2707
|
+
* the commission flow assigned, a /prefix from the netmask, the subnet's `.1`
|
|
2708
|
+
* as gateway, and the configured DNS.
|
|
2709
|
+
* @param {object} params
|
|
2710
|
+
* @param {string} params.ipAddress - Static IP for the deployed OS.
|
|
2711
|
+
* @param {string} params.netmask - Dotted netmask (e.g. 255.255.255.0).
|
|
2712
|
+
* @param {string} params.dnsServer - DNS server.
|
|
2713
|
+
* @returns {{ netIp: string, netPrefix: number, netGateway: string, netDns: string }}
|
|
2714
|
+
* @memberof UnderpostBaremetal
|
|
2715
|
+
*/
|
|
2716
|
+
resolveInstalledNetwork({ ipAddress, netmask, dnsServer }) {
|
|
2717
|
+
const prefix = `${netmask || ''}`
|
|
2718
|
+
.split('.')
|
|
2719
|
+
.reduce((acc, oct) => acc + ((parseInt(oct, 10).toString(2).match(/1/g) || []).length || 0), 0);
|
|
2720
|
+
const netGateway = `${ipAddress || ''}`.replace(/\.\d+$/, '.1');
|
|
2721
|
+
return { netIp: ipAddress || '', netPrefix: prefix || 24, netGateway, netDns: dnsServer || '8.8.8.8' };
|
|
2722
|
+
},
|
|
2723
|
+
|
|
2724
|
+
/**
|
|
2725
|
+
* @method commissioningWriteFilesFactory
|
|
2726
|
+
* @description Generates the write_files configuration for the commissioning script.
|
|
2727
|
+
* @param {object} params
|
|
2728
|
+
* @param {object} params.machine - The machine object.
|
|
2729
|
+
* @param {object} params.authCredentials - MAAS authentication credentials.
|
|
2730
|
+
* @param {string} params.runnerHostIp - The IP address of the runner host.
|
|
2731
|
+
* @memberof UnderpostBaremetal
|
|
2732
|
+
* @returns {Array} The write_files array.
|
|
2733
|
+
*/
|
|
2734
|
+
commissioningWriteFilesFactory({ machine, authCredentials, runnerHostIp }) {
|
|
2735
|
+
const { consumer_key, token_key, token_secret } = authCredentials;
|
|
2736
|
+
return [
|
|
2737
|
+
{
|
|
2738
|
+
path: '/usr/local/bin/underpost-enlist.sh',
|
|
2739
|
+
permissions: '0755',
|
|
2740
|
+
owner: 'root:root',
|
|
2741
|
+
content: `#!/bin/bash
|
|
2742
|
+
# set -euo pipefail
|
|
2743
|
+
CONSUMER_KEY="${consumer_key}"
|
|
2744
|
+
TOKEN_KEY="${token_key}"
|
|
2745
|
+
TOKEN_SECRET="${token_secret}"
|
|
2746
|
+
LOG_FILE="/var/log/underpost-enlistment.log"
|
|
2747
|
+
RESPONSE_FILE="/tmp/maas_response.txt"
|
|
2748
|
+
STATUS_FILE="/tmp/maas_status.txt"
|
|
2749
|
+
|
|
2750
|
+
echo "Starting MAAS Commissioning Request..." | tee -a "$LOG_FILE"
|
|
2751
|
+
|
|
2752
|
+
curl -X POST \\
|
|
2753
|
+
--location --verbose \\
|
|
2754
|
+
--header "Authorization: OAuth oauth_version=\\"1.0\\", oauth_signature_method=\\"PLAINTEXT\\", oauth_consumer_key=\\"$CONSUMER_KEY\\", oauth_token=\\"$TOKEN_KEY\\", oauth_signature=\\"&$TOKEN_SECRET\\", oauth_nonce=\\"$(uuidgen)\\", oauth_timestamp=\\"$(date +%s)\\"" \\
|
|
2755
|
+
-F "enable_ssh=1" \\
|
|
2756
|
+
http://${runnerHostIp}:5240/MAAS/api/2.0/machines/${machine.system_id}/op-commission \\
|
|
2757
|
+
--output "$RESPONSE_FILE" --write-out "%{http_code}" > "$STATUS_FILE" 2>> "$LOG_FILE"
|
|
2758
|
+
|
|
2759
|
+
HTTP_STATUS=$(cat "$STATUS_FILE")
|
|
2760
|
+
|
|
2761
|
+
echo "HTTP Status: $HTTP_STATUS" | tee -a "$LOG_FILE"
|
|
2762
|
+
echo "Response Body:" | tee -a "$LOG_FILE"
|
|
2763
|
+
cat "$RESPONSE_FILE" | tee -a "$LOG_FILE"
|
|
2764
|
+
|
|
2765
|
+
if [ "$HTTP_STATUS" -eq 200 ]; then
|
|
2766
|
+
echo "Commissioning requested successfully." | tee -a "$LOG_FILE"
|
|
2767
|
+
else
|
|
2768
|
+
echo "ERROR: MAAS commissioning failed with status $HTTP_STATUS" | tee -a "$LOG_FILE"
|
|
2769
|
+
exit 0
|
|
2770
|
+
fi
|
|
2771
|
+
`,
|
|
2772
|
+
},
|
|
2773
|
+
];
|
|
2774
|
+
},
|
|
2775
|
+
|
|
2776
|
+
/**
|
|
2777
|
+
* @method httpBootstrapServerStaticFactory
|
|
2778
|
+
* @description Creates static files for the bootstrap HTTP server including cloud-init configuration.
|
|
2779
|
+
* @param {object} params - Parameters for creating static files.
|
|
2780
|
+
* @param {string} params.bootstrapHttpServerPath - The path where static files will be created.
|
|
2781
|
+
* @param {string} params.hostname - The hostname of the client machine.
|
|
2782
|
+
* @param {string} params.cloudConfigSrc - The cloud-init configuration YAML source.
|
|
2783
|
+
* @param {string} params.kickstartSrc - The kickstart configuration content.
|
|
2784
|
+
* @param {string} params.vendorData - The cloud-init vendor-data content.
|
|
2785
|
+
* @param {string} params.isoUrl - Optional ISO URL to cache and serve.
|
|
2786
|
+
* @memberof UnderpostBaremetal
|
|
2787
|
+
* @returns {void}
|
|
2788
|
+
*/
|
|
2789
|
+
httpBootstrapServerStaticFactory({
|
|
2790
|
+
bootstrapHttpServerPath = '',
|
|
2791
|
+
hostname = '',
|
|
2792
|
+
cloudConfigSrc = '',
|
|
2793
|
+
kickstartSrc = '',
|
|
2794
|
+
vendorData = '',
|
|
2795
|
+
isoUrl = '',
|
|
2796
|
+
}) {
|
|
2797
|
+
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}`);
|
|
2798
|
+
|
|
2799
|
+
Underpost.cloudInit.httpServerStaticFactory({ bootstrapHttpServerPath, hostname, cloudConfigSrc, vendorData });
|
|
2800
|
+
Underpost.kickstart.httpServerStaticFactory({ bootstrapHttpServerPath, hostname, kickstartSrc });
|
|
2801
|
+
|
|
2802
|
+
if (isoUrl) {
|
|
2803
|
+
const isoFilename = isoUrl.split('/').pop();
|
|
2804
|
+
const isoCacheDir = `/var/tmp/live-iso`;
|
|
2805
|
+
const isoCachePath = `${isoCacheDir}/${isoFilename}`;
|
|
2806
|
+
const isoDestPath = `${bootstrapHttpServerPath}/${hostname}/${isoFilename}`;
|
|
2807
|
+
|
|
2808
|
+
if (!fs.existsSync(isoCachePath)) {
|
|
2809
|
+
logger.info(`Downloading ISO to cache: ${isoUrl}`);
|
|
2810
|
+
shellExec(`mkdir -p ${isoCacheDir}`);
|
|
2811
|
+
shellExec(`wget --progress=bar:force -O ${isoCachePath} "${isoUrl}"`);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
logger.info(`Copying ISO to bootstrap server: ${isoDestPath}`);
|
|
2815
|
+
shellExec(`cp ${isoCachePath} ${isoDestPath}`);
|
|
2816
|
+
}
|
|
2817
|
+
},
|
|
2818
|
+
|
|
2819
|
+
/**
|
|
2820
|
+
* @method httpBootstrapServerRunnerFactory
|
|
2821
|
+
* @description Starts a simple HTTP server to serve boot files for network booting.
|
|
2822
|
+
* @param {object} options - Options for the HTTP server.
|
|
2823
|
+
* @param {string} hostname - The hostname of the client machine.
|
|
2824
|
+
* @param {string} options.bootstrapHttpServerPath - The path to serve files from (default: './public/localhost').
|
|
2825
|
+
* @param {number} options.bootstrapHttpServerPort - The port on which to start the HTTP server (default: 8888).
|
|
2826
|
+
* @memberof UnderpostBaremetal
|
|
2827
|
+
* @returns {void}
|
|
2828
|
+
*/
|
|
2829
|
+
httpBootstrapServerRunnerFactory(
|
|
2830
|
+
options = { hostname: 'localhost', bootstrapHttpServerPath: './public/localhost', bootstrapHttpServerPort: 8888 },
|
|
2831
|
+
) {
|
|
2832
|
+
const port = options.bootstrapHttpServerPort || 8888;
|
|
2833
|
+
const bootstrapHttpServerPath = options.bootstrapHttpServerPath || './public/localhost';
|
|
2834
|
+
const hostname = options.hostname || 'localhost';
|
|
2835
|
+
|
|
2836
|
+
shellExec(`node bin run kill ${port}`, { silent: true });
|
|
2837
|
+
|
|
2838
|
+
const app = express();
|
|
2839
|
+
|
|
2840
|
+
app.use(loggerMiddleware(import.meta, 'debug', () => false));
|
|
2841
|
+
|
|
2842
|
+
// Lifecycle event sink. The ephemeral Kickstart/Anaconda runtime POSTs
|
|
2843
|
+
// stage transitions (ssh-ready, installing, completed, …) here so the
|
|
2844
|
+
// controller can observe progress without polling the machine itself.
|
|
2845
|
+
// Events are kept in-memory (consumed by the orchestration step) and
|
|
2846
|
+
// mirrored to disk for auditing.
|
|
2847
|
+
app.use(express.json({ limit: '256kb' }));
|
|
2848
|
+
app.post('/:hostname/status', (req, res) => {
|
|
2849
|
+
const reqHostname = req.params.hostname;
|
|
2850
|
+
const event = {
|
|
2851
|
+
...(req.body && typeof req.body === 'object' ? req.body : {}),
|
|
2852
|
+
receivedAt: new Date().toISOString(),
|
|
2853
|
+
remoteIp: (req.headers['x-forwarded-for'] || req.socket?.remoteAddress || '').toString(),
|
|
2854
|
+
};
|
|
2855
|
+
const stage = event.stage || 'unknown';
|
|
2856
|
+
|
|
2857
|
+
const events = UnderpostBaremetal.bootstrapStatusEvents.get(reqHostname) || [];
|
|
2858
|
+
events.push(event);
|
|
2859
|
+
UnderpostBaremetal.bootstrapStatusEvents.set(reqHostname, events);
|
|
2860
|
+
|
|
2861
|
+
try {
|
|
2862
|
+
const statusDir = `${bootstrapHttpServerPath}/${reqHostname}/status`;
|
|
2863
|
+
fs.ensureDirSync(statusDir);
|
|
2864
|
+
fs.writeFileSync(`${statusDir}/${stage}.json`, JSON.stringify(event, null, 2), 'utf8');
|
|
2865
|
+
fs.appendFileSync(`${statusDir}/events.log`, `${JSON.stringify(event)}\n`, 'utf8');
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
logger.warn('Failed to persist bootstrap status event', { error: error.message });
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
logger.info(`Bootstrap status event [${reqHostname}] stage=${stage}`, event);
|
|
2871
|
+
res.json({ ok: true, stage });
|
|
2872
|
+
});
|
|
2873
|
+
|
|
2874
|
+
app.use('/', express.static(bootstrapHttpServerPath));
|
|
2875
|
+
|
|
2876
|
+
app.listen(port, () => {
|
|
2877
|
+
logger.info(`Static file server running on port ${port}`);
|
|
2878
|
+
});
|
|
2879
|
+
|
|
2880
|
+
// Configure iptables to allow incoming LAN connections
|
|
2881
|
+
shellExec(
|
|
2882
|
+
`sudo iptables -I INPUT 1 -p tcp -s 192.168.1.0/24 --dport ${port} -m conntrack --ctstate NEW -j ACCEPT`,
|
|
2883
|
+
);
|
|
2884
|
+
// Option for any host:
|
|
2885
|
+
// sudo iptables -I INPUT 1 -p tcp --dport ${port} -m conntrack --ctstate NEW -j ACCEPT
|
|
2886
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${bootstrapHttpServerPath}`);
|
|
2887
|
+
shellExec(`sudo sudo chmod 755 ${bootstrapHttpServerPath}`);
|
|
2888
|
+
|
|
2889
|
+
logger.info(`Started Bootstrap Http Server on port ${port}`);
|
|
2890
|
+
},
|
|
2891
|
+
|
|
2892
|
+
/**
|
|
2893
|
+
* @method updateKernelFiles
|
|
2894
|
+
* @description Copies EFI bootloaders, kernel, and initrd images to the TFTP root path.
|
|
2895
|
+
* It also handles decompression of the kernel if necessary for ARM64 compatibility,
|
|
2896
|
+
* and extracts raw kernel images from PE32+ EFI wrappers (common in Rocky Linux ARM64).
|
|
2897
|
+
* @param {object} params - The parameters for the function.
|
|
2898
|
+
* @param {object} params.commissioningImage - The commissioning image configuration.
|
|
2899
|
+
* @param {string} params.resourcesPath - The path where resources are located.
|
|
2900
|
+
* @param {string} params.tftpRootPath - The TFTP root path.
|
|
2901
|
+
* @param {object} params.kernelFilesPaths - Paths to kernel files.
|
|
2902
|
+
* @memberof UnderpostBaremetal
|
|
2903
|
+
* @returns {void}
|
|
2904
|
+
*/
|
|
2905
|
+
updateKernelFiles({ commissioningImage, resourcesPath, tftpRootPath, kernelFilesPaths }) {
|
|
2906
|
+
// Copy EFI bootloaders to TFTP path.
|
|
2907
|
+
const arch = resourcesPath.split('/').pop();
|
|
2908
|
+
const efiFiles =
|
|
2909
|
+
arch === 'arm64' || arch === 'aarch64' ? ['bootaa64.efi', 'grubaa64.efi'] : ['bootx64.efi', 'grubx64.efi'];
|
|
2910
|
+
|
|
2911
|
+
for (const file of efiFiles) {
|
|
2912
|
+
shellExec(`sudo cp -a ${resourcesPath}/${file} ${tftpRootPath}/pxe/${file}`);
|
|
2913
|
+
}
|
|
2914
|
+
// Copy kernel and initrd images to TFTP path.
|
|
2915
|
+
for (const file of Object.keys(kernelFilesPaths)) {
|
|
2916
|
+
if (file == 'isoUrl') continue; // Skip URL entries
|
|
2917
|
+
shellExec(`sudo cp -a ${kernelFilesPaths[file]} ${tftpRootPath}/pxe/${file}`);
|
|
2918
|
+
// If the file is a kernel (vmlinuz-efi) and is gzipped, unzip it for GRUB compatibility on ARM64.
|
|
2919
|
+
// GRUB on ARM64 often crashes with synchronous exception (0x200) if handling large compressed kernels directly.
|
|
2920
|
+
if (file === 'vmlinuz-efi') {
|
|
2921
|
+
const kernelDest = `${tftpRootPath}/pxe/${file}`;
|
|
2922
|
+
const fileType = shellExec(`file ${kernelDest}`).stdout;
|
|
2923
|
+
|
|
2924
|
+
// Handle gzip compressed kernels
|
|
2925
|
+
if (fileType.includes('gzip compressed data')) {
|
|
2926
|
+
logger.info(`Decompressing kernel ${file} for ARM64 UEFI compatibility...`);
|
|
2927
|
+
shellExec(`sudo mv ${kernelDest} ${kernelDest}.gz`);
|
|
2928
|
+
shellExec(`sudo gunzip ${kernelDest}.gz`);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// Handle PE32+ EFI wrapped kernels (common in Rocky Linux ARM64)
|
|
2932
|
+
// Rocky Linux ARM64 kernels are distributed as PE32+ EFI executables, which
|
|
2933
|
+
// are bootable directly via UEFI firmware. However, GRUB's 'linux' command
|
|
2934
|
+
// expects a raw ARM64 Linux kernel Image format, not a PE32+ wrapper.
|
|
2935
|
+
if (fileType.includes('PE32+') || fileType.includes('EFI application')) {
|
|
2936
|
+
logger.warn('Detected PE32+ EFI wrapped kernel. Need to extract raw kernel image for GRUB.');
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
},
|
|
2941
|
+
|
|
2942
|
+
/**
|
|
2943
|
+
* @method kernelCmdBootParamsFactory
|
|
2944
|
+
* @description Constructs kernel command line parameters for NFS booting.
|
|
2945
|
+
* @param {object} options - Options for constructing the command line.
|
|
2946
|
+
* @param {string} options.ipClient - The IP address of the client.
|
|
2947
|
+
* @param {string} options.ipDhcpServer - The IP address of the DHCP server.
|
|
2948
|
+
* @param {string} options.ipFileServer - The IP address of the file server.
|
|
2949
|
+
* @param {string} options.ipConfig - The IP configuration method (e.g., 'dhcp').
|
|
2950
|
+
* @param {string} options.netmask - The network mask.
|
|
2951
|
+
* @param {string} options.hostname - The hostname of the client.
|
|
2952
|
+
* @param {string} options.dnsServer - The DNS server address.
|
|
2953
|
+
* @param {string} options.networkInterfaceName - The name of the network interface.
|
|
2954
|
+
* @param {string} options.fileSystemUrl - The URL of the root filesystem.
|
|
2955
|
+
* @param {number} options.bootstrapHttpServerPort - The port of the bootstrap HTTP server.
|
|
2956
|
+
* @param {string} options.type - The type of boot ('iso-ram', 'chroot-debootstrap', 'chroot-container', 'iso-nfs', etc.).
|
|
2957
|
+
* @param {string} options.macAddress - The MAC address of the client.
|
|
2958
|
+
* @param {boolean} options.cloudInit - Whether to include cloud-init parameters.
|
|
2959
|
+
* @param {object} options.machine - The machine object containing system_id.
|
|
2960
|
+
* @param {boolean} [options.dev=false] - Whether to enable dev mode with dracut debugging parameters.
|
|
2961
|
+
* @param {string} [options.osIdLike=''] - OS family identifier (e.g., 'rhel centos fedora' or 'debian ubuntu').
|
|
2962
|
+
* @param {object} options.authCredentials - Authentication credentials for fetching files (if needed).
|
|
2963
|
+
* @param {string} options.authCredentials.consumer_key - Consumer key for authentication.
|
|
2964
|
+
* @param {string} options.authCredentials.consumer_secret - Consumer secret for authentication.
|
|
2965
|
+
* @param {string} options.authCredentials.token_key - Token key for authentication.
|
|
2966
|
+
* @param {string} options.authCredentials.token_secret - Token secret for authentication.
|
|
2967
|
+
* @param {string} options.architecture - The architecture of the machine (e.g., 'amd64', 'arm64').
|
|
2968
|
+
* @returns {object} An object containing the constructed command line string.
|
|
2969
|
+
* @memberof UnderpostBaremetal
|
|
2970
|
+
*/
|
|
2971
|
+
kernelCmdBootParamsFactory(
|
|
2972
|
+
options = {
|
|
2973
|
+
ipClient: '',
|
|
2974
|
+
ipDhcpServer: '',
|
|
2975
|
+
ipFileServer: '',
|
|
2976
|
+
ipConfig: '',
|
|
2977
|
+
netmask: '',
|
|
2978
|
+
hostname: '',
|
|
2979
|
+
dnsServer: '',
|
|
2980
|
+
networkInterfaceName: '',
|
|
2981
|
+
fileSystemUrl: '',
|
|
2982
|
+
bootstrapHttpServerPort: 8888,
|
|
2983
|
+
type: '',
|
|
2984
|
+
macAddress: '',
|
|
2985
|
+
cloudInit: false,
|
|
2986
|
+
dev: false,
|
|
2987
|
+
osIdLike: '',
|
|
2988
|
+
authCredentials: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' },
|
|
2989
|
+
architecture,
|
|
2990
|
+
},
|
|
2991
|
+
) {
|
|
2992
|
+
// Construct kernel command line arguments for NFS boot.
|
|
2993
|
+
const {
|
|
2994
|
+
ipClient,
|
|
2995
|
+
ipDhcpServer,
|
|
2996
|
+
ipFileServer,
|
|
2997
|
+
ipConfig,
|
|
2998
|
+
netmask,
|
|
2999
|
+
hostname,
|
|
3000
|
+
dnsServer,
|
|
3001
|
+
networkInterfaceName,
|
|
3002
|
+
fileSystemUrl,
|
|
3003
|
+
bootstrapHttpServerPort,
|
|
3004
|
+
type,
|
|
3005
|
+
macAddress,
|
|
3006
|
+
cloudInit,
|
|
3007
|
+
osIdLike,
|
|
3008
|
+
architecture,
|
|
3009
|
+
} = options;
|
|
3010
|
+
|
|
3011
|
+
// Determine OS family from osIdLike
|
|
3012
|
+
const { isDebianBased, isRhelBased } = Underpost.baremetal.getFamilyBaseOs(options.osIdLike);
|
|
3013
|
+
|
|
3014
|
+
const ifaceName = networkInterfaceName ? networkInterfaceName : 'eth0';
|
|
3015
|
+
const isStaticIp = ipConfig === 'none' || ipConfig === 'off';
|
|
3016
|
+
const ipParam = isStaticIp
|
|
3017
|
+
? `ip=${ipClient}:${ipFileServer}:${ipDhcpServer}:${netmask}:${hostname}:${ifaceName}:${ipConfig}:${dnsServer}`
|
|
3018
|
+
: `ip=::::${hostname}:${ifaceName}:${ipConfig}`;
|
|
3019
|
+
|
|
3020
|
+
let nfsMountOptions = [];
|
|
3021
|
+
if (type === 'chroot-debootstrap' || type === 'chroot-container') {
|
|
3022
|
+
nfsMountOptions = [
|
|
3023
|
+
'tcp',
|
|
3024
|
+
'nfsvers=3',
|
|
3025
|
+
'nolock',
|
|
3026
|
+
'port=2049',
|
|
3027
|
+
'hard',
|
|
3028
|
+
'intr',
|
|
3029
|
+
'rsize=32768',
|
|
3030
|
+
'wsize=32768',
|
|
3031
|
+
'acregmin=0',
|
|
3032
|
+
'acregmax=0',
|
|
3033
|
+
'acdirmin=0',
|
|
3034
|
+
'acdirmax=0',
|
|
3035
|
+
'noac',
|
|
3036
|
+
];
|
|
3037
|
+
} else if (type === 'iso-nfs' && isDebianBased) {
|
|
3038
|
+
nfsMountOptions = ['nolock', 'nfsvers=3', 'tcp', 'hard', 'port=2049', 'rsize=32768', 'wsize=32768'];
|
|
3039
|
+
}
|
|
3040
|
+
const nfsOptions = nfsMountOptions.join(',');
|
|
3041
|
+
const nfsServerPath = `${ipFileServer}:${process.env.NFS_EXPORT_PATH}/${hostname}`;
|
|
3042
|
+
const nfsRootParam = `nfsroot=${nfsServerPath}${nfsOptions ? `,${nfsOptions}` : ''}`;
|
|
3043
|
+
const casperNfsParams = [`nfsroot=${nfsServerPath}`, ...(nfsOptions ? [`nfsopts=${nfsOptions}`] : [])];
|
|
3044
|
+
const permissionsParams = [
|
|
3045
|
+
`rw`,
|
|
3046
|
+
// `ro`
|
|
3047
|
+
];
|
|
3048
|
+
|
|
3049
|
+
const kernelParams = [
|
|
3050
|
+
...permissionsParams,
|
|
3051
|
+
`ignore_uuid`,
|
|
3052
|
+
`rootwait`,
|
|
3053
|
+
`ipv6.disable=1`,
|
|
3054
|
+
`fixrtc`,
|
|
3055
|
+
// `console=serial0,115200`,
|
|
3056
|
+
// `console=tty1`,
|
|
3057
|
+
// `layerfs-path=filesystem.squashfs`,
|
|
3058
|
+
// `root=/dev/ram0`,
|
|
3059
|
+
// `toram`,
|
|
3060
|
+
// 'nomodeset',
|
|
3061
|
+
// `editable_rootfs=tmpfs`, // all writes to rootfs go to RAM, keeping underlying storage pristine
|
|
3062
|
+
// `ramdisk_size=3550000`,
|
|
3063
|
+
// `root=/dev/sda1`, // rpi4 usb port unit
|
|
3064
|
+
'apparmor=0', // Disable AppArmor security
|
|
3065
|
+
...(networkInterfaceName === 'eth0'
|
|
3066
|
+
? [
|
|
3067
|
+
'net.ifnames=0', // Disable predictable network interface names
|
|
3068
|
+
'biosdevname=0', // Disable BIOS device naming
|
|
3069
|
+
]
|
|
3070
|
+
: []),
|
|
3071
|
+
];
|
|
3072
|
+
|
|
3073
|
+
const performanceParams = [
|
|
3074
|
+
// --- Boot Automation & Stability ---
|
|
3075
|
+
'auto=true', // Enable automated installation/configuration
|
|
3076
|
+
'noeject', // Do not attempt to eject boot media on reboot
|
|
3077
|
+
`casper-getty`, // Enable console login for live sessions
|
|
3078
|
+
'nowatchdog', // Disable watchdog timers to prevent unexpected reboots
|
|
3079
|
+
'noprompt', // Don't wait for "Press Enter" during boot/reboot
|
|
3080
|
+
|
|
3081
|
+
// --- CPU & System Performance ---
|
|
3082
|
+
'mitigations=off', // Disable CPU security mitigations for maximum speed
|
|
3083
|
+
'clocksource=tsc', // Use fastest available hardware clock
|
|
3084
|
+
'tsc=reliable', // Trust CPU clock without extra verification
|
|
3085
|
+
'hpet=disable', // Disable legacy slow timer
|
|
3086
|
+
'nohz=on', // Reduce overhead by disabling timer ticks on idle CPUs
|
|
3087
|
+
|
|
3088
|
+
// --- Memory & Hardware Optimization ---
|
|
3089
|
+
'cma=40M', // Reserve contiguous RAM for RPi hardware/video
|
|
3090
|
+
'zswap.enabled=1', // Use compressed RAM cache (vital for NFS/SD)
|
|
3091
|
+
'zswap.compressor=zstd', // Best balance of speed and compression
|
|
3092
|
+
'zswap.max_pool_percent=30', // Use max 30% of RAM as compressed storage
|
|
3093
|
+
'zswap.zpool=zsmalloc', // Efficient memory management for zswap
|
|
3094
|
+
'fsck.mode=skip', // Skip disk checks to accelerate boot
|
|
3095
|
+
'max_loop=255', // Ensure enough loop devices for squashfs/snaps
|
|
3096
|
+
|
|
3097
|
+
// --- Immutable Filesystem ---
|
|
3098
|
+
'overlayroot=tmpfs', // Run entire OS in RAM to protect storage
|
|
3099
|
+
'overlayroot_cfgdisk=disabled', // Ignore external overlay configurations
|
|
3100
|
+
];
|
|
3101
|
+
|
|
3102
|
+
let cmd = [];
|
|
3103
|
+
if (type === 'iso-ram') {
|
|
3104
|
+
if (isRhelBased) {
|
|
3105
|
+
cmd = Underpost.kickstart.kernelParamsFactory(macAddress, [ipParam, ...kernelParams], options);
|
|
3106
|
+
} else {
|
|
3107
|
+
// ISO-RAM (Debian/Ubuntu): full live ISO downloaded into RAM via casper toram.
|
|
3108
|
+
const netBootParams = [`netboot=url`];
|
|
3109
|
+
if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
|
|
3110
|
+
cmd = [ipParam, `boot=casper`, 'toram', ...netBootParams, ...kernelParams, ...performanceParams];
|
|
3111
|
+
}
|
|
3112
|
+
} else if (type === 'chroot-debootstrap' || type === 'chroot-container') {
|
|
3113
|
+
let qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`];
|
|
3114
|
+
cmd = [ipParam, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
|
|
3115
|
+
} else {
|
|
3116
|
+
// 'iso-nfs' — kernel/initrd from ISO, root filesystem served via NFS.
|
|
3117
|
+
if (isDebianBased) {
|
|
3118
|
+
cmd = [ipParam, `boot=casper`, `netboot=nfs`, ...casperNfsParams, ...kernelParams];
|
|
3119
|
+
} else {
|
|
3120
|
+
cmd = [ipParam, `netboot=nfs`, nfsRootParam, ...kernelParams, ...performanceParams];
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// Add RHEL/Rocky/Fedora based images specific parameters
|
|
3125
|
+
if (isRhelBased) {
|
|
3126
|
+
cmd = cmd.concat([`rd.neednet=1`, `rd.timeout=180`, `selinux=0`, `enforcing=0`]);
|
|
3127
|
+
if (options.dev) cmd = cmd.concat([`rd.shell`, `rd.debug`]);
|
|
3128
|
+
}
|
|
3129
|
+
// Add Debian/Ubuntu based images specific parameters
|
|
3130
|
+
else if (isDebianBased) {
|
|
3131
|
+
if (type !== 'iso-nfs') cmd = cmd.concat([`initrd=initrd.img`, `init=/sbin/init`]);
|
|
3132
|
+
if (options.dev) cmd = cmd.concat([`debug`, `ignore_loglevel`]);
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
if (cloudInit) cmd = Underpost.cloudInit.kernelParamsFactory(macAddress, cmd, options);
|
|
3136
|
+
// cmd.push('---');
|
|
3137
|
+
const cmdStr = cmd.join(' ');
|
|
3138
|
+
logger.info('Constructed kernel command line');
|
|
3139
|
+
console.log(newInstance(cmdStr).bgRed.bold.black);
|
|
3140
|
+
return { cmd: cmdStr };
|
|
3141
|
+
},
|
|
3142
|
+
|
|
3143
|
+
/**
|
|
3144
|
+
* @method waitForBootstrapStage
|
|
3145
|
+
* @description Polls the in-memory bootstrap status events (populated by the
|
|
3146
|
+
* bootstrap HTTP server POST sink) until the target stage is reported by the
|
|
3147
|
+
* ephemeral runtime, or the timeout elapses.
|
|
3148
|
+
* @param {object} params
|
|
3149
|
+
* @param {string} params.hostname - Hostname key the runtime reports under.
|
|
3150
|
+
* @param {string} params.stage - Stage to wait for (e.g. 'ssh-ready').
|
|
3151
|
+
* @param {number} [params.timeoutMs=900000] - Maximum wait time in ms.
|
|
3152
|
+
* @param {number} [params.intervalMs=2000] - Poll interval in ms.
|
|
3153
|
+
* @returns {Promise<object|null>} The matching event, or null on timeout.
|
|
3154
|
+
* @memberof UnderpostBaremetal
|
|
3155
|
+
*/
|
|
3156
|
+
async waitForBootstrapStage({ hostname, stage, timeoutMs = 15 * 60 * 1000, intervalMs = 2000 }) {
|
|
3157
|
+
const deadline = Date.now() + timeoutMs;
|
|
3158
|
+
let lastSeenCount = -1;
|
|
3159
|
+
let lastHeartbeat = 0;
|
|
3160
|
+
const heartbeatMs = 15000;
|
|
3161
|
+
while (Date.now() < deadline) {
|
|
3162
|
+
const events = UnderpostBaremetal.bootstrapStatusEvents.get(hostname) || [];
|
|
3163
|
+
const match = events.find((e) => e.stage === stage);
|
|
3164
|
+
if (match) return match;
|
|
3165
|
+
|
|
3166
|
+
const now = Date.now();
|
|
3167
|
+
if (events.length !== lastSeenCount || now - lastHeartbeat >= heartbeatMs) {
|
|
3168
|
+
lastSeenCount = events.length;
|
|
3169
|
+
lastHeartbeat = now;
|
|
3170
|
+
const remainingSec = Math.max(0, Math.round((deadline - now) / 1000));
|
|
3171
|
+
logger.info(`Waiting for stage '${stage}' from ${hostname}`, {
|
|
3172
|
+
stagesSeen: events.map((e) => e.stage),
|
|
3173
|
+
eventCount: events.length,
|
|
3174
|
+
remainingSec,
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
await timer(intervalMs);
|
|
3178
|
+
}
|
|
3179
|
+
logger.warn(`Timed out waiting for bootstrap stage '${stage}' from ${hostname}`, {
|
|
3180
|
+
stagesSeen: (UnderpostBaremetal.bootstrapStatusEvents.get(hostname) || []).map((e) => e.stage),
|
|
3181
|
+
});
|
|
3182
|
+
return null;
|
|
3183
|
+
},
|
|
3184
|
+
|
|
3185
|
+
/**
|
|
3186
|
+
* @method commissionMonitor
|
|
3187
|
+
* @description Monitors the MAAS discoveries and initiates machine creation and commissioning
|
|
3188
|
+
* once a matching MAC address is found. It also opens terminal windows for live logs.
|
|
3189
|
+
* @param {object} params - The parameters for the function.
|
|
3190
|
+
* @param {string} params.macAddress - The MAC address to monitor for.
|
|
3191
|
+
* @param {string} params.ipAddress - The IP address of the machine (used if MAC is all zeros).
|
|
3192
|
+
* @param {string} [params.hostname] - The hostname for the machine (optional).
|
|
3193
|
+
* @param {string} [params.architecture] - The architecture of the machine (optional).
|
|
3194
|
+
* @param {object} [params.machine] - Existing machine payload to use (optional).
|
|
3195
|
+
* @returns {Promise<void>} A promise object with machine and discovery details.
|
|
3196
|
+
* @memberof UnderpostBaremetal
|
|
3197
|
+
*/
|
|
3198
|
+
async commissionMonitor({ macAddress, ipAddress, hostname, architecture, machine }) {
|
|
3199
|
+
{
|
|
3200
|
+
// Query observed discoveries from MAAS.
|
|
3201
|
+
const discoveries = Underpost.baremetal.maasCliExec(`discoveries read`);
|
|
3202
|
+
|
|
3203
|
+
for (const discovery of discoveries) {
|
|
3204
|
+
const discoverHostname = discovery.hostname
|
|
3205
|
+
? discovery.hostname
|
|
3206
|
+
: discovery.mac_organization
|
|
3207
|
+
? discovery.mac_organization
|
|
3208
|
+
: discovery.domain
|
|
3209
|
+
? discovery.domain
|
|
3210
|
+
: `generic-host-${s4()}${s4()}`;
|
|
3211
|
+
|
|
3212
|
+
console.log(discoverHostname.bgBlue.bold.white);
|
|
3213
|
+
console.log('ip target:'.green + ipAddress, 'ip discovered:'.green + discovery.ip);
|
|
3214
|
+
console.log('mac target:'.green + macAddress, 'mac discovered:'.green + discovery.mac_address);
|
|
3215
|
+
|
|
3216
|
+
if (discovery.ip === ipAddress) {
|
|
3217
|
+
logger.info('Machine discovered!', discovery);
|
|
3218
|
+
if (!machine) {
|
|
3219
|
+
// Check if a machine with the discovered MAC already exists to avoid conflicts
|
|
3220
|
+
const [existingMachine] =
|
|
3221
|
+
Underpost.baremetal.maasCliExec(`machines read mac_address=${discovery.mac_address}`) || [];
|
|
3222
|
+
|
|
3223
|
+
if (existingMachine) {
|
|
3224
|
+
logger.warn(
|
|
3225
|
+
`Machine ${existingMachine.hostname} (${existingMachine.system_id}) already exists with MAC ${discovery.mac_address}`,
|
|
3226
|
+
);
|
|
3227
|
+
logger.info(
|
|
3228
|
+
`Deleting existing machine ${existingMachine.system_id} to create new machine ${hostname}...`,
|
|
3229
|
+
);
|
|
3230
|
+
Underpost.baremetal.maasCliExec(`machine delete ${existingMachine.system_id}`);
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
logger.info('Creating new machine with discovered hardware MAC...', {
|
|
3234
|
+
discoveredMAC: discovery.mac_address,
|
|
3235
|
+
ipAddress,
|
|
3236
|
+
hostname,
|
|
3237
|
+
});
|
|
3238
|
+
machine = Underpost.baremetal.machineFactory({
|
|
3239
|
+
ipAddress,
|
|
3240
|
+
macAddress: discovery.mac_address,
|
|
3241
|
+
hostname,
|
|
3242
|
+
architecture,
|
|
3243
|
+
}).machine;
|
|
3244
|
+
|
|
3245
|
+
if (machine && machine.system_id) {
|
|
3246
|
+
console.log('New machine system id:', machine.system_id.bgYellow.bold.black);
|
|
3247
|
+
Underpost.baremetal.writeGrubConfigToFile({
|
|
3248
|
+
grubCfgSrc: Underpost.baremetal
|
|
3249
|
+
.getGrubConfigFromFile()
|
|
3250
|
+
.grubCfgSrc.replaceAll('system-id', machine.system_id),
|
|
3251
|
+
});
|
|
3252
|
+
} else {
|
|
3253
|
+
logger.error('Failed to create machine or obtain system_id', machine);
|
|
3254
|
+
throw new Error('Machine creation failed');
|
|
3255
|
+
}
|
|
3256
|
+
} else {
|
|
3257
|
+
const systemId = machine.system_id;
|
|
3258
|
+
console.log('Using pre-registered machine system_id:', systemId.bgYellow.bold.black);
|
|
3259
|
+
|
|
3260
|
+
// Update the boot interface MAC if hardware MAC differs from pre-registered MAC
|
|
3261
|
+
// This handles both hardware mode (macAddress is null) and MAC mismatch scenarios
|
|
3262
|
+
if (macAddress === null || macAddress !== discovery.mac_address) {
|
|
3263
|
+
logger.info('Updating machine interface with discovered hardware MAC...', {
|
|
3264
|
+
preRegisteredMAC: macAddress || 'none (hardware mode)',
|
|
3265
|
+
discoveredMAC: discovery.mac_address,
|
|
3266
|
+
});
|
|
3267
|
+
|
|
3268
|
+
// Check current machine status before attempting state transitions
|
|
3269
|
+
const currentMachine = Underpost.baremetal.maasCliExec(`machine read ${systemId}`);
|
|
3270
|
+
const currentStatus = currentMachine ? currentMachine.status_name : 'Unknown';
|
|
3271
|
+
logger.info('Current machine status before interface update:', { systemId, status: currentStatus });
|
|
3272
|
+
|
|
3273
|
+
// Only mark-broken if the machine is in a state that supports it (e.g. Ready, New, Allocated)
|
|
3274
|
+
// Machines already in Broken state don't need to be marked broken again
|
|
3275
|
+
if (currentStatus !== 'Broken') {
|
|
3276
|
+
try {
|
|
3277
|
+
Underpost.baremetal.maasCliExec(`machine mark-broken ${systemId}`);
|
|
3278
|
+
logger.info('Machine marked as broken successfully');
|
|
3279
|
+
} catch (markBrokenError) {
|
|
3280
|
+
logger.warn('Failed to mark machine as broken, attempting interface update anyway...', {
|
|
3281
|
+
error: markBrokenError.message,
|
|
3282
|
+
currentStatus,
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
} else {
|
|
3286
|
+
logger.info('Machine is already in Broken state, skipping mark-broken');
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
Underpost.baremetal.maasCliExec(
|
|
3290
|
+
`interface update ${systemId} ${machine.boot_interface.id}` + ` mac_address=${discovery.mac_address}`,
|
|
3291
|
+
);
|
|
3292
|
+
|
|
3293
|
+
// Re-check status before mark-fixed — only attempt if actually Broken
|
|
3294
|
+
const updatedMachine = Underpost.baremetal.maasCliExec(`machine read ${systemId}`);
|
|
3295
|
+
const updatedStatus = updatedMachine ? updatedMachine.status_name : 'Unknown';
|
|
3296
|
+
|
|
3297
|
+
if (updatedStatus === 'Broken') {
|
|
3298
|
+
try {
|
|
3299
|
+
Underpost.baremetal.maasCliExec(`machine mark-fixed ${systemId}`);
|
|
3300
|
+
logger.info('Machine marked as fixed successfully');
|
|
3301
|
+
} catch (markFixedError) {
|
|
3302
|
+
logger.warn('Failed to mark machine as fixed:', { error: markFixedError.message });
|
|
3303
|
+
}
|
|
3304
|
+
} else {
|
|
3305
|
+
logger.info('Machine is not in Broken state, skipping mark-fixed', { status: updatedStatus });
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
logger.info('✓ Machine interface MAC address updated successfully');
|
|
3309
|
+
|
|
3310
|
+
// commissioning_scripts=90-verify-user.sh
|
|
3311
|
+
}
|
|
3312
|
+
logger.info('Machine resource uri', machine.resource_uri);
|
|
3313
|
+
for (const iface of machine.interface_set)
|
|
3314
|
+
logger.info('Interface info', {
|
|
3315
|
+
name: iface.name,
|
|
3316
|
+
mac_address: iface.mac_address,
|
|
3317
|
+
resource_uri: iface.resource_uri,
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
return { discovery, machine };
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
await timer(1000);
|
|
3325
|
+
return await Underpost.baremetal.commissionMonitor({
|
|
3326
|
+
macAddress,
|
|
3327
|
+
ipAddress,
|
|
3328
|
+
hostname,
|
|
3329
|
+
architecture,
|
|
3330
|
+
machine,
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
},
|
|
3334
|
+
|
|
3335
|
+
/**
|
|
3336
|
+
* @method maasCliExec
|
|
3337
|
+
* @description Executes a MAAS CLI command and returns the parsed JSON output.
|
|
3338
|
+
* This method abstracts the execution of MAAS CLI commands, ensuring that the output is captured and parsed correctly.
|
|
3339
|
+
* @param {string} cmd - The MAAS CLI command to execute (e.g., 'machines read').
|
|
3340
|
+
* @returns {object|null} The parsed JSON output from the MAAS CLI command, or null if there is no output.
|
|
3341
|
+
* @memberof UnderpostBaremetal
|
|
3342
|
+
*/
|
|
3343
|
+
maasCliExec(cmd) {
|
|
3344
|
+
const output = shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} ${cmd}`, {
|
|
3345
|
+
stdout: true,
|
|
3346
|
+
silent: true,
|
|
3347
|
+
}).trim();
|
|
3348
|
+
try {
|
|
3349
|
+
return output ? JSON.parse(output) : null;
|
|
3350
|
+
} catch (error) {
|
|
3351
|
+
console.log('output', output);
|
|
3352
|
+
logger.error(error);
|
|
3353
|
+
throw error;
|
|
3354
|
+
}
|
|
3355
|
+
},
|
|
3356
|
+
|
|
3357
|
+
/**
|
|
3358
|
+
* @method maasAuthCredentialsFactory
|
|
3359
|
+
* @description Retrieves MAAS API key credentials from the MAAS CLI.
|
|
3360
|
+
* This method parses the output of `maas apikey` to extract the consumer key,
|
|
3361
|
+
* consumer secret, token key, and token secret.
|
|
3362
|
+
* @returns {object} An object containing the MAAS authentication credentials.
|
|
3363
|
+
* @memberof UnderpostBaremetal
|
|
3364
|
+
* @throws {Error} If the MAAS API key format is invalid.
|
|
3365
|
+
*/
|
|
3366
|
+
maasAuthCredentialsFactory() {
|
|
3367
|
+
// Expected formats:
|
|
3368
|
+
// <consumer_key>:<consumer_token>:<secret> (older format)
|
|
3369
|
+
// <consumer_key>:<consumer_secret>:<token_key>:<token_secret> (newer format)
|
|
3370
|
+
// Commands used to generate API keys:
|
|
3371
|
+
// maas apikey --with-names --username ${process.env.MAAS_ADMIN_USERNAME}
|
|
3372
|
+
// maas ${process.env.MAAS_ADMIN_USERNAME} account create-authorisation-token
|
|
3373
|
+
// maas apikey --generate --username ${process.env.MAAS_ADMIN_USERNAME}
|
|
3374
|
+
// Reference: https://github.com/CanonicalLtd/maas-docs/issues/647
|
|
3375
|
+
|
|
3376
|
+
const parts = shellExec(`maas apikey --with-names --username ${process.env.MAAS_ADMIN_USERNAME}`, {
|
|
3377
|
+
stdout: true,
|
|
3378
|
+
})
|
|
3379
|
+
.trim()
|
|
3380
|
+
.split(`\n`)[0] // Take only the first line of output.
|
|
3381
|
+
.split(':'); // Split by colon to get individual parts.
|
|
3382
|
+
|
|
3383
|
+
let consumer_key, consumer_secret, token_key, token_secret;
|
|
3384
|
+
|
|
3385
|
+
// Determine the format of the API key and assign parts accordingly.
|
|
3386
|
+
if (parts.length === 4) {
|
|
3387
|
+
[consumer_key, consumer_secret, token_key, token_secret] = parts;
|
|
3388
|
+
} else if (parts.length === 3) {
|
|
3389
|
+
// Handle older 3-part format, setting consumer_secret as empty.
|
|
3390
|
+
[consumer_key, token_key, token_secret] = parts;
|
|
3391
|
+
consumer_secret = '';
|
|
3392
|
+
token_secret = token_secret.split(' MAAS consumer')[0].trim(); // Clean up token secret.
|
|
3393
|
+
} else {
|
|
3394
|
+
// Throw an error if the format is not recognized.
|
|
3395
|
+
throw new Error('Invalid token format');
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
logger.info('Maas api token generated', { consumer_key, consumer_secret, token_key, token_secret });
|
|
3399
|
+
return { consumer_key, consumer_secret, token_key, token_secret };
|
|
3400
|
+
},
|
|
3401
|
+
|
|
3402
|
+
/**
|
|
3403
|
+
* @method mountBinfmtMisc
|
|
3404
|
+
* @description Mounts the binfmt_misc filesystem to enable QEMU user-static binfmt support.
|
|
3405
|
+
* This is necessary for cross-architecture execution within a chroot environment.
|
|
3406
|
+
* @param {object} params - The parameters for the function.
|
|
3407
|
+
* @memberof UnderpostBaremetal
|
|
3408
|
+
* @returns {void}
|
|
3409
|
+
*/
|
|
3410
|
+
mountBinfmtMisc() {
|
|
3411
|
+
// Install necessary packages for debootstrap and QEMU.
|
|
3412
|
+
shellExec(`sudo dnf install -y iptables-legacy`);
|
|
3413
|
+
shellExec(`sudo dnf install -y debootstrap`);
|
|
3414
|
+
shellExec(`sudo dnf install -y kernel-modules-extra-$(uname -r)`);
|
|
3415
|
+
// Reset QEMU user-static binfmt for proper cross-architecture execution.
|
|
3416
|
+
shellExec(`sudo podman run --rm --privileged docker.io/multiarch/qemu-user-static:latest --reset -p yes`);
|
|
3417
|
+
// Mount binfmt_misc filesystem.
|
|
3418
|
+
shellExec(`sudo modprobe binfmt_misc`);
|
|
3419
|
+
shellExec(`sudo mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc`, { silentOnError: true });
|
|
3420
|
+
},
|
|
3421
|
+
|
|
3422
|
+
/**
|
|
3423
|
+
* @method removeMachines
|
|
3424
|
+
* @description Deletes all specified machines from MAAS.
|
|
3425
|
+
* @param {object} params - The parameters for the function.
|
|
3426
|
+
* @param {Array<object>} params.machines - An array of machine objects, each with a `system_id`.
|
|
3427
|
+
* @param {Array<string>} [params.ignore] - An optional array of system IDs to ignore during deletion.
|
|
3428
|
+
* @memberof UnderpostBaremetal
|
|
3429
|
+
* @returns {Array<object>} An empty array after machines are removed.
|
|
3430
|
+
*/
|
|
3431
|
+
removeMachines({ machines, ignore }) {
|
|
3432
|
+
for (const machine of machines) {
|
|
3433
|
+
// Handle both string system_ids and machine objects
|
|
3434
|
+
const systemId = typeof machine === 'string' ? machine : machine.system_id;
|
|
3435
|
+
if (ignore && ignore.find((mId) => mId === systemId)) continue;
|
|
3436
|
+
logger.info(`Removing machine: ${systemId}`);
|
|
3437
|
+
Underpost.baremetal.maasCliExec(`machine delete ${systemId}`);
|
|
3438
|
+
}
|
|
3439
|
+
return [];
|
|
3440
|
+
},
|
|
3441
|
+
|
|
3442
|
+
/**
|
|
3443
|
+
* @method clearDiscoveries
|
|
3444
|
+
* @description Clears all observed discoveries in MAAS and optionally forces a new scan.
|
|
3445
|
+
* @param {object} params - The parameters for the function.
|
|
3446
|
+
* @param {boolean} params.force - If true, forces a new discovery scan after clearing.
|
|
3447
|
+
* @memberof UnderpostBaremetal
|
|
3448
|
+
* @returns {void}
|
|
3449
|
+
*/
|
|
3450
|
+
clearDiscoveries({ force }) {
|
|
3451
|
+
Underpost.baremetal.maasCliExec(`discoveries clear all=true`);
|
|
3452
|
+
if (force === true) {
|
|
3453
|
+
Underpost.baremetal.maasCliExec(`discoveries scan force=true`);
|
|
3454
|
+
}
|
|
3455
|
+
},
|
|
3456
|
+
|
|
3457
|
+
/**
|
|
3458
|
+
* @method macMonitor
|
|
3459
|
+
* @description Monitors for the presence of a MAC address file in the NFS host path.
|
|
3460
|
+
* This is used to wait for the target machine to report its MAC address.
|
|
3461
|
+
* @param {object} params - The parameters for the function.
|
|
3462
|
+
* @param {string} params.nfsHostPath - The NFS host path where the MAC file is expected.
|
|
3463
|
+
* @memberof UnderpostBaremetal
|
|
3464
|
+
* @returns {Promise<void>} A promise that resolves when the MAC file is found or after a delay.
|
|
3465
|
+
*/
|
|
3466
|
+
async macMonitor({ nfsHostPath }) {
|
|
3467
|
+
if (fs.existsSync(`${nfsHostPath}/underpost/mac`)) {
|
|
3468
|
+
const mac = fs.readFileSync(`${nfsHostPath}/underpost/mac`, 'utf8').trim();
|
|
3469
|
+
logger.info('Commissioning MAC', mac);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
await timer(1000);
|
|
3473
|
+
await Underpost.baremetal.macMonitor({ nfsHostPath });
|
|
3474
|
+
},
|
|
3475
|
+
|
|
3476
|
+
/**
|
|
3477
|
+
* @method installGrubModules
|
|
3478
|
+
* @description Installs the necessary GRUB modules for both ARM64 and AMD64 architectures.
|
|
3479
|
+
* This ensures that the GRUB bootloader can properly load the kernel and initrd images
|
|
3480
|
+
* during the network boot process, regardless of the target architecture.
|
|
3481
|
+
* @memberof UnderpostBaremetal
|
|
3482
|
+
* @returns {void}
|
|
3483
|
+
*/
|
|
3484
|
+
installGrubModules() {
|
|
3485
|
+
if (!fs.existsSync('/usr/lib/grub/x86_64-efi')) shellExec(`sudo dnf install -y grub2-efi-x64-modules`);
|
|
3486
|
+
if (!fs.existsSync('/usr/lib/grub/arm64-efi')) shellExec(`sudo dnf install -y grub2-efi-aa64-modules`);
|
|
3487
|
+
},
|
|
3488
|
+
|
|
3489
|
+
/**
|
|
3490
|
+
* @method crossArchBinFactory
|
|
3491
|
+
* @description Copies the appropriate QEMU static binary into the NFS root filesystem
|
|
3492
|
+
* for cross-architecture execution within a chroot environment.
|
|
3493
|
+
* @param {object} params - The parameters for the function.
|
|
3494
|
+
* @param {string} params.nfsHostPath - The path to the NFS root filesystem on the host.
|
|
3495
|
+
* @memberof UnderpostBaremetal
|
|
3496
|
+
* @param {'arm64'|'amd64'} params.debootstrapArch - The target architecture of the debootstrap environment.
|
|
3497
|
+
* @returns {void}
|
|
3498
|
+
*/
|
|
3499
|
+
crossArchBinFactory({ nfsHostPath, bootstrapArch }) {
|
|
3500
|
+
switch (bootstrapArch) {
|
|
3501
|
+
case 'arm64':
|
|
3502
|
+
// Copy QEMU static binary for ARM64.
|
|
3503
|
+
shellExec(`sudo podman cp extract:/usr/bin/qemu-aarch64-static ${nfsHostPath}/usr/bin/`);
|
|
3504
|
+
break;
|
|
3505
|
+
case 'amd64':
|
|
3506
|
+
// Copy QEMU static binary for AMD64.
|
|
3507
|
+
shellExec(`sudo podman cp extract:/usr/bin/qemu-x86_64-static ${nfsHostPath}/usr/bin/`);
|
|
3508
|
+
break;
|
|
3509
|
+
default:
|
|
3510
|
+
// Log a warning or throw an error for unsupported architectures.
|
|
3511
|
+
logger.warn(`Unsupported bootstrap architecture: ${bootstrapArch}`);
|
|
3512
|
+
break;
|
|
3513
|
+
}
|
|
3514
|
+
},
|
|
3515
|
+
|
|
3516
|
+
/**
|
|
3517
|
+
* @method crossArchRunner
|
|
3518
|
+
* @description Executes a series of shell commands within a chroot environment,
|
|
3519
|
+
* optionally using QEMU for cross-architecture execution.
|
|
3520
|
+
* @param {object} params - The parameters for the function.
|
|
3521
|
+
* @param {string} params.nfsHostPath - The path to the NFS root filesystem on the host.
|
|
3522
|
+
* @param {'arm64'|'amd64'} params.debootstrapArch - The target architecture of the debootstrap environment.
|
|
3523
|
+
* @param {object} params.callbackMetaData - Metadata about the callback, including runner host architecture.
|
|
3524
|
+
* @memberof UnderpostBaremetal
|
|
3525
|
+
* @param {string[]} params.steps - An array of shell commands to execute.
|
|
3526
|
+
* @returns {void}
|
|
3527
|
+
*/
|
|
3528
|
+
crossArchRunner({ nfsHostPath, bootstrapArch, callbackMetaData, steps }) {
|
|
3529
|
+
// Render the steps with logging for better visibility during execution.
|
|
3530
|
+
steps = Underpost.baremetal.stepsRender(steps, false);
|
|
3531
|
+
|
|
3532
|
+
let qemuCrossArchBash = '';
|
|
3533
|
+
// Determine if QEMU is needed for cross-architecture execution.
|
|
3534
|
+
if (bootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
3535
|
+
switch (bootstrapArch) {
|
|
3536
|
+
case 'arm64':
|
|
3537
|
+
qemuCrossArchBash = '/usr/bin/qemu-aarch64-static ';
|
|
3538
|
+
break;
|
|
3539
|
+
case 'amd64':
|
|
3540
|
+
qemuCrossArchBash = '/usr/bin/qemu-x86_64-static ';
|
|
3541
|
+
break;
|
|
3542
|
+
default:
|
|
3543
|
+
// No QEMU prefix for unsupported or native architectures.
|
|
3544
|
+
break;
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
// Execute the commands within the chroot environment using a heredoc.
|
|
3548
|
+
shellExec(`sudo chroot ${nfsHostPath} ${qemuCrossArchBash}/bin/bash <<'EOF'
|
|
3549
|
+
${steps}
|
|
3550
|
+
EOF`);
|
|
3551
|
+
},
|
|
3552
|
+
|
|
3553
|
+
/**
|
|
3554
|
+
* @method stepsRender
|
|
3555
|
+
* @description Renders an array of shell commands into a formatted string,
|
|
3556
|
+
* optionally including YAML-style formatting and execution logging.
|
|
3557
|
+
* This helps in visualizing and debugging the execution flow of provisioning steps.
|
|
3558
|
+
* @param {string[]} [steps=[]] - An array of shell commands.
|
|
3559
|
+
* @param {boolean} [yaml=true] - If true, formats the output as YAML list items.
|
|
3560
|
+
* @memberof UnderpostBaremetal
|
|
3561
|
+
* @returns {string} The formatted string of commands.
|
|
3562
|
+
*/
|
|
3563
|
+
stepsRender(steps = [], yaml = true) {
|
|
3564
|
+
return steps
|
|
3565
|
+
.map(
|
|
3566
|
+
(step, i, a) =>
|
|
3567
|
+
// Add a timestamp and step counter for better logging and traceability.
|
|
3568
|
+
(yaml ? ' - ' : '') +
|
|
3569
|
+
'echo "' +
|
|
3570
|
+
(yaml ? '\\' : '') +
|
|
3571
|
+
'$(date) | ' +
|
|
3572
|
+
(i + 1) +
|
|
3573
|
+
'/' +
|
|
3574
|
+
a.length +
|
|
3575
|
+
' - ' +
|
|
3576
|
+
step.split('\n')[0] +
|
|
3577
|
+
'"' +
|
|
3578
|
+
`\n` +
|
|
3579
|
+
`${yaml ? ' - ' : ''}${step}`,
|
|
3580
|
+
)
|
|
3581
|
+
.join('\n');
|
|
3582
|
+
},
|
|
3583
|
+
|
|
3584
|
+
/**
|
|
3585
|
+
* @method nfsMountCallback
|
|
3586
|
+
* @description Manages NFS mounts and unmounts for the baremetal provisioning process.
|
|
3587
|
+
* It checks the mount status and performs mount/unmount operations as requested.
|
|
3588
|
+
* @param {object} params - The parameters for the function.
|
|
3589
|
+
* @param {string} params.hostname - The hostname of the target machine.
|
|
3590
|
+
* @param {string} params.nfsHostPath - The NFS host path for the target machine.
|
|
3591
|
+
* @param {string} params.workflowId - The identifier for the workflow configuration.
|
|
3592
|
+
* @param {boolean} [params.mount] - If true, attempts to mount the NFS paths.
|
|
3593
|
+
* @param {boolean} [params.unmount] - If true, attempts to unmount the NFS paths.
|
|
3594
|
+
* @param {number} [currentRecall=0] - The current recall attempt count for retries.
|
|
3595
|
+
* @param {number} [maxRecalls=5] - The maximum number of recall attempts allowed.
|
|
3596
|
+
* @memberof UnderpostBaremetal
|
|
3597
|
+
* @returns {Promise<void>} A promise that resolves when the mount/unmount operations are complete.
|
|
3598
|
+
*/
|
|
3599
|
+
async nfsMountCallback({ hostname, nfsHostPath, workflowId, mount, unmount }, currentRecall = 0, maxRecalls = 5) {
|
|
3600
|
+
// Mount binfmt_misc filesystem.
|
|
3601
|
+
if (mount) Underpost.baremetal.mountBinfmtMisc();
|
|
3602
|
+
const unMountCmds = [];
|
|
3603
|
+
const mountCmds = [];
|
|
3604
|
+
const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
|
|
3605
|
+
let recall = false;
|
|
3606
|
+
if (!workflowsConfig[workflowId]) {
|
|
3607
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
3608
|
+
}
|
|
3609
|
+
if (
|
|
3610
|
+
workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
|
|
3611
|
+
workflowsConfig[workflowId].type === 'chroot-container'
|
|
3612
|
+
) {
|
|
3613
|
+
const mounts = {
|
|
3614
|
+
bind: ['/proc', '/sys', '/run'],
|
|
3615
|
+
rbind: ['/dev'],
|
|
3616
|
+
};
|
|
3617
|
+
|
|
3618
|
+
for (const mountCmd of Object.keys(mounts)) {
|
|
3619
|
+
for (const mountPath of mounts[mountCmd]) {
|
|
3620
|
+
const hostMountPath = `${process.env.NFS_EXPORT_PATH}/${hostname}${mountPath}`;
|
|
3621
|
+
// Check if the path is already mounted using `mountpoint` command.
|
|
3622
|
+
// `mountpoint` exits 1 when the path is not a mountpoint — silentOnError
|
|
3623
|
+
// prevents ShellExecError so we can inspect stdout/stderr for the string.
|
|
3624
|
+
const mountpointOut = shellExec(`mountpoint ${hostMountPath}`, {
|
|
3625
|
+
silent: true,
|
|
3626
|
+
stdout: true,
|
|
3627
|
+
silentOnError: true,
|
|
3628
|
+
});
|
|
3629
|
+
const isPathMounted =
|
|
3630
|
+
typeof mountpointOut === 'string' && mountpointOut.length > 0
|
|
3631
|
+
? !mountpointOut.match('not a mountpoint') && !mountpointOut.match('No such file')
|
|
3632
|
+
: false;
|
|
3633
|
+
|
|
3634
|
+
if (isPathMounted) {
|
|
3635
|
+
logger.warn('Nfs path already mounted', mountPath);
|
|
3636
|
+
if (unmount === true) {
|
|
3637
|
+
// Unmount if requested.
|
|
3638
|
+
unMountCmds.push(`sudo umount -Rfl ${hostMountPath}`);
|
|
3639
|
+
if (!recall) recall = true;
|
|
3640
|
+
}
|
|
3641
|
+
} else {
|
|
3642
|
+
if (mount === true) {
|
|
3643
|
+
// Mount if requested and not already mounted.
|
|
3644
|
+
mountCmds.push(`sudo mount --${mountCmd} ${mountPath} ${hostMountPath}`);
|
|
3645
|
+
} else {
|
|
3646
|
+
logger.warn('Nfs path not mounted', mountPath);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
for (const unMountCmd of unMountCmds) shellExec(unMountCmd);
|
|
3652
|
+
if (recall) {
|
|
3653
|
+
if (currentRecall >= maxRecalls) {
|
|
3654
|
+
throw new Error(
|
|
3655
|
+
`Maximum recall attempts (${maxRecalls}) reached for nfsMountCallback. Hostname: ${hostname}`,
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
logger.info(`nfsMountCallback recall attempt ${currentRecall + 1}/${maxRecalls} for hostname: ${hostname}`);
|
|
3659
|
+
await timer(1000);
|
|
3660
|
+
return await Underpost.baremetal.nfsMountCallback(
|
|
3661
|
+
{ hostname, nfsHostPath, workflowId, mount, unmount },
|
|
3662
|
+
currentRecall + 1,
|
|
3663
|
+
maxRecalls,
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
if (mountCmds.length > 0) {
|
|
3667
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${nfsHostPath}`);
|
|
3668
|
+
shellExec(`sudo chmod -R 755 ${nfsHostPath}`);
|
|
3669
|
+
for (const mountCmd of mountCmds) shellExec(mountCmd);
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
},
|
|
3673
|
+
|
|
3674
|
+
/**
|
|
3675
|
+
* @method getHostArch
|
|
3676
|
+
* @description Determines the architecture of the host machine.
|
|
3677
|
+
* This is crucial for cross-compilation and selecting the correct QEMU binaries.
|
|
3678
|
+
* @memberof UnderpostBaremetal
|
|
3679
|
+
* @returns {{alias: 'amd64'|'arm64', name: 'x86_64'|'aarch64'}} The host architecture.
|
|
3680
|
+
* @throws {Error} If the host architecture is unsupported.
|
|
3681
|
+
*/
|
|
3682
|
+
getHostArch() {
|
|
3683
|
+
// `uname -m` returns e.g. 'x86_64' or 'aarch64'
|
|
3684
|
+
const machine = shellExec('uname -m', { stdout: true, silent: true }).trim();
|
|
3685
|
+
if (machine === 'x86_64') return { alias: 'amd64', name: 'x86_64' };
|
|
3686
|
+
if (machine === 'aarch64') return { alias: 'arm64', name: 'aarch64' };
|
|
3687
|
+
throw new Error(`Unsupported host architecture: ${machine}`);
|
|
3688
|
+
},
|
|
3689
|
+
|
|
3690
|
+
/**
|
|
3691
|
+
* @method rebuildNfsServer
|
|
3692
|
+
* @description Configures NFS exports and aligns host firewall/NFS daemon ports for MAAS workflows.
|
|
3693
|
+
* @param {object} params - The parameters for the function.
|
|
3694
|
+
* @param {string} params.nfsHostPath - The path to the NFS server export.
|
|
3695
|
+
* @memberof UnderpostBaremetal
|
|
3696
|
+
* @param {string} [params.subnet='192.168.1.0/24'] - The subnet allowed to access the NFS export.
|
|
3697
|
+
* @param {boolean} [params.nfsReset=false] - Flag to completely reset the NFS server (restart service).
|
|
3698
|
+
* @param {string} [params.underpostRoot] - Repository root used to locate helper scripts.
|
|
3699
|
+
* @returns {void}
|
|
3700
|
+
*/
|
|
3701
|
+
rebuildNfsServer({ nfsHostPath, subnet, nfsReset, underpostRoot }) {
|
|
3702
|
+
if (!subnet) subnet = '192.168.1.0/24'; // Default subnet if not provided.
|
|
3703
|
+
if (!underpostRoot) {
|
|
3704
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
3705
|
+
underpostRoot = path.resolve(__dirname, '../..');
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
const maasNatFirewalldPath = path.resolve(underpostRoot, 'scripts/maas-nat-firewalld.sh');
|
|
3709
|
+
const nfsPorts = UnderpostBaremetal.NFS_V3_PORTS;
|
|
3710
|
+
const exportOptions = ['rw', 'sync', 'no_root_squash', 'no_subtree_check', 'insecure'].join(',');
|
|
3711
|
+
|
|
3712
|
+
if (!fs.existsSync(maasNatFirewalldPath)) {
|
|
3713
|
+
throw new Error(`MAAS firewalld helper not found: ${maasNatFirewalldPath}`);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
// Write the NFS exports configuration to /etc/exports.
|
|
3717
|
+
if (!fs.existsSync(nfsHostPath)) fs.mkdirSync(nfsHostPath, { recursive: true });
|
|
3718
|
+
fs.writeFileSync(`/etc/exports`, `${nfsHostPath} ${subnet}(${exportOptions})`, 'utf8');
|
|
3719
|
+
|
|
3720
|
+
logger.info('Configuring MAAS firewalld and fixed NFSv3 ports...');
|
|
3721
|
+
shellExec(
|
|
3722
|
+
[
|
|
3723
|
+
`MAAS_LAN_CIDR=${subnet}`,
|
|
3724
|
+
`NFS_MODE=v3`,
|
|
3725
|
+
`CONFIGURE_NFS_V3_PORTS=true`,
|
|
3726
|
+
`NFS_MOUNTD_PORT=${nfsPorts.mountd}`,
|
|
3727
|
+
`NFS_STATD_PORT=${nfsPorts.statd}`,
|
|
3728
|
+
`NFS_STATD_OUTGOING_PORT=${nfsPorts.statdOutgoing}`,
|
|
3729
|
+
`NFS_LOCKD_PORT=${nfsPorts.lockd}`,
|
|
3730
|
+
`NFS_LOCKD_UDP_PORT=${nfsPorts.lockd}`,
|
|
3731
|
+
`bash "${maasNatFirewalldPath}"`,
|
|
3732
|
+
].join(' '),
|
|
3733
|
+
);
|
|
3734
|
+
|
|
3735
|
+
logger.info('Reloading NFS exports...');
|
|
3736
|
+
shellExec(`sudo exportfs -rav`);
|
|
3737
|
+
|
|
3738
|
+
// Display the currently active NFS exports for verification.
|
|
3739
|
+
logger.info('Displaying active NFS exports');
|
|
3740
|
+
shellExec(`sudo exportfs -s`);
|
|
3741
|
+
|
|
3742
|
+
if (nfsReset) {
|
|
3743
|
+
logger.info('Restarting NFS server service...');
|
|
3744
|
+
let restarted = false;
|
|
3745
|
+
for (const unit of ['nfs-server', 'nfs-kernel-server']) {
|
|
3746
|
+
const result = shellExec(`sudo systemctl restart ${unit}`, { silentOnError: true });
|
|
3747
|
+
if (result.code === 0) {
|
|
3748
|
+
restarted = true;
|
|
3749
|
+
logger.info(`NFS server restarted via ${unit}.`);
|
|
3750
|
+
break;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
if (!restarted) logger.warn('Unable to restart nfs-server or nfs-kernel-server after export reload.');
|
|
3754
|
+
}
|
|
3755
|
+
},
|
|
3756
|
+
|
|
3757
|
+
/**
|
|
3758
|
+
* @method checkQemuCrossArchSupport
|
|
3759
|
+
* @description Checks for QEMU support when building for a different architecture.
|
|
3760
|
+
* This is essential for validator bots that need to build images for architectures
|
|
3761
|
+
* different from the host system (e.g., building arm64 on x86_64 or vice versa).
|
|
3762
|
+
* @param {object} workflow - The workflow configuration object.
|
|
3763
|
+
* @param {object} workflow.maas - The MAAS configuration.
|
|
3764
|
+
* @param {string} workflow.maas.architecture - Target architecture (e.g., 'arm64/generic', 'amd64/generic').
|
|
3765
|
+
* @memberof UnderpostBaremetal
|
|
3766
|
+
* @throws {Error} If QEMU is not installed or doesn't support required machine types.
|
|
3767
|
+
* @returns {void}
|
|
3768
|
+
*/
|
|
3769
|
+
checkQemuCrossArchSupport(workflow) {
|
|
3770
|
+
// Check for QEMU support if building for a different architecture (validator bots case)
|
|
3771
|
+
if (workflow.maas.architecture.startsWith('arm64') && process.arch !== 'arm64') {
|
|
3772
|
+
// Building arm64/aarch64 on x86_64 host
|
|
3773
|
+
// Check both /usr/local/bin (compiled) and system paths
|
|
3774
|
+
let qemuAarch64Path = null;
|
|
3775
|
+
|
|
3776
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64', { silentOnError: true }).code === 0) {
|
|
3777
|
+
qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
|
|
3778
|
+
} else if (shellExec('which qemu-system-aarch64', { silentOnError: true }).code === 0) {
|
|
3779
|
+
qemuAarch64Path = shellExec('which qemu-system-aarch64', { stdout: true }).trim();
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
if (!qemuAarch64Path) {
|
|
3783
|
+
throw new Error(
|
|
3784
|
+
'qemu-system-aarch64 is not installed. Please install it to build ARM64 images on x86_64 hosts.\n' +
|
|
3785
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
logger.info(`Found qemu-system-aarch64 at: ${qemuAarch64Path}`);
|
|
3790
|
+
|
|
3791
|
+
// Verify that the installed qemu supports the 'virt' machine type (required for arm64)
|
|
3792
|
+
const machineHelp = shellExec(`${qemuAarch64Path} -machine help`).stdout;
|
|
3793
|
+
if (!machineHelp.includes('virt')) {
|
|
3794
|
+
throw new Error(
|
|
3795
|
+
'The installed qemu-system-aarch64 does not support the "virt" machine type.\n' +
|
|
3796
|
+
'This usually happens if qemu-system-aarch64 is a symlink to qemu-kvm on x86_64.\n' +
|
|
3797
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
3798
|
+
);
|
|
3799
|
+
}
|
|
3800
|
+
} else if (workflow.maas.architecture.startsWith('amd64') && process.arch !== 'x64') {
|
|
3801
|
+
// Building amd64/x86_64 on aarch64 host
|
|
3802
|
+
// Check both /usr/local/bin (compiled) and system paths
|
|
3803
|
+
let qemuX86Path = null;
|
|
3804
|
+
|
|
3805
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64', { silentOnError: true }).code === 0) {
|
|
3806
|
+
qemuX86Path = '/usr/local/bin/qemu-system-x86_64';
|
|
3807
|
+
} else if (shellExec('which qemu-system-x86_64', { silentOnError: true }).code === 0) {
|
|
3808
|
+
qemuX86Path = shellExec('which qemu-system-x86_64', { stdout: true }).trim();
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
if (!qemuX86Path) {
|
|
3812
|
+
throw new Error(
|
|
3813
|
+
'qemu-system-x86_64 is not installed. Please install it to build x86_64 images on aarch64 hosts.\n' +
|
|
3814
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
3815
|
+
);
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
logger.info(`Found qemu-system-x86_64 at: ${qemuX86Path}`);
|
|
3819
|
+
|
|
3820
|
+
// Verify that the installed qemu supports the 'pc' or 'q35' machine type (required for x86_64)
|
|
3821
|
+
const machineHelp = shellExec(`${qemuX86Path} -machine help`).stdout;
|
|
3822
|
+
if (!machineHelp.includes('pc') && !machineHelp.includes('q35')) {
|
|
3823
|
+
throw new Error(
|
|
3824
|
+
'The installed qemu-system-x86_64 does not support the "pc" or "q35" machine type.\n' +
|
|
3825
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
3826
|
+
);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
},
|
|
3830
|
+
|
|
3831
|
+
/**
|
|
3832
|
+
* @method bootConfFactory
|
|
3833
|
+
* @description Generates the boot configuration file for specific workflows,
|
|
3834
|
+
* primarily for Raspberry Pi 4 Model B. This configuration includes TFTP settings,
|
|
3835
|
+
* MAC address override, and static IP configuration.
|
|
3836
|
+
* @param {object} params - The parameters for the function.
|
|
3837
|
+
* @param {string} params.workflowId - The identifier for the specific workflow.
|
|
3838
|
+
* @param {string} params.tftpIp - The IP address of the TFTP server.
|
|
3839
|
+
* @param {string} params.tftpPrefixStr - The TFTP prefix string for boot files.
|
|
3840
|
+
* @param {string} params.macAddress - The MAC address to be set for the device.
|
|
3841
|
+
* @param {string} params.clientIp - The static IP address for the client device.
|
|
3842
|
+
* @param {string} params.subnet - The subnet mask for the client device.
|
|
3843
|
+
* @param {string} params.gateway - The gateway IP address for the client device.
|
|
3844
|
+
* @memberof UnderpostBaremetal
|
|
3845
|
+
* @returns {string} The generated boot configuration content.
|
|
3846
|
+
* @throws {Error} If an invalid workflow ID is provided.
|
|
3847
|
+
*/
|
|
3848
|
+
bootConfFactory({ workflowId, tftpIp, tftpPrefixStr, macAddress, clientIp, subnet, gateway }) {
|
|
3849
|
+
if (workflowId.startsWith('rpi4mb')) {
|
|
3850
|
+
// Instructions: Flash sd with Raspberry Pi OS lite and update:
|
|
3851
|
+
// EEPROM (Electrically Erasable Programmable Read-Only Memory) like microcontrollers
|
|
3852
|
+
// sudo rpi-eeprom-config --apply /boot/firmware/boot.conf
|
|
3853
|
+
// sudo reboot
|
|
3854
|
+
// vcgencmd bootloader_config
|
|
3855
|
+
// shutdown -h now
|
|
3856
|
+
return `[all]
|
|
3857
|
+
BOOT_UART=0
|
|
3858
|
+
WAKE_ON_GPIO=1
|
|
3859
|
+
POWER_OFF_ON_HALT=0
|
|
3860
|
+
ENABLE_SELF_UPDATE=1
|
|
3861
|
+
DISABLE_HDMI=0
|
|
3862
|
+
NET_INSTALL_ENABLED=1
|
|
3863
|
+
DHCP_TIMEOUT=45000
|
|
3864
|
+
DHCP_REQ_TIMEOUT=4000
|
|
3865
|
+
TFTP_FILE_TIMEOUT=30000
|
|
3866
|
+
BOOT_ORDER=0x21
|
|
3867
|
+
|
|
3868
|
+
# ─────────────────────────────────────────────────────────────
|
|
3869
|
+
# TFTP configuration
|
|
3870
|
+
# ─────────────────────────────────────────────────────────────
|
|
3871
|
+
|
|
3872
|
+
# Custom TFTP prefix string (e.g., based on MAC address, no colons)
|
|
3873
|
+
#TFTP_PREFIX_STR=AA-BB-CC-DD-EE-FF/
|
|
3874
|
+
|
|
3875
|
+
# Optional PXE Option43 override (leave commented if unused)
|
|
3876
|
+
#PXE_OPTION43="Raspberry Pi Boot"
|
|
3877
|
+
|
|
3878
|
+
# DHCP client GUID (Option 97); 0x34695052 is the FourCC for Raspberry Pi 4
|
|
3879
|
+
#DHCP_OPTION97=0x34695052
|
|
3880
|
+
|
|
3881
|
+
TFTP_IP=${tftpIp}
|
|
3882
|
+
TFTP_PREFIX=1
|
|
3883
|
+
TFTP_PREFIX_STR=${tftpPrefixStr}/
|
|
3884
|
+
|
|
3885
|
+
# ─────────────────────────────────────────────────────────────
|
|
3886
|
+
# Manually override Ethernet MAC address
|
|
3887
|
+
# ─────────────────────────────────────────────────────────────
|
|
3888
|
+
|
|
3889
|
+
#MAC_ADDRESS=${macAddress}
|
|
3890
|
+
|
|
3891
|
+
# OTP MAC address override
|
|
3892
|
+
#MAC_ADDRESS_OTP=0,1
|
|
3893
|
+
|
|
3894
|
+
# ─────────────────────────────────────────────────────────────
|
|
3895
|
+
# Static IP configuration (bypasses DHCP completely)
|
|
3896
|
+
# ─────────────────────────────────────────────────────────────
|
|
3897
|
+
#CLIENT_IP=${clientIp}
|
|
3898
|
+
SUBNET=${subnet}
|
|
3899
|
+
GATEWAY=${gateway}`;
|
|
3900
|
+
} else logger.warn(`No boot configuration factory defined for workflow ID: ${workflowId}`);
|
|
3901
|
+
},
|
|
3902
|
+
|
|
3903
|
+
/**
|
|
3904
|
+
* @method loadWorkflowsConfig
|
|
3905
|
+
* @description Loads the commission workflows configuration from commission-workflows.json.
|
|
3906
|
+
* Each workflow defines specific parameters like system provisioning type,
|
|
3907
|
+
* kernel version, Chrony settings, debootstrap image details, and NFS mounts. *
|
|
3908
|
+
* @memberof UnderpostBaremetal
|
|
3909
|
+
*/
|
|
3910
|
+
loadWorkflowsConfig() {
|
|
3911
|
+
if (this._workflowsConfig) return this._workflowsConfig;
|
|
3912
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
3913
|
+
const configPath = path.resolve(__dirname, '../../baremetal/commission-workflows.json');
|
|
3914
|
+
this._workflowsConfig = fs.readJsonSync(configPath);
|
|
3915
|
+
return this._workflowsConfig;
|
|
3916
|
+
},
|
|
3917
|
+
|
|
3918
|
+
/**
|
|
3919
|
+
* @property {object} packerMaasImageBuildWorkflows
|
|
3920
|
+
* @description Configuration for PACKe mass image workflows.
|
|
3921
|
+
* @memberof UnderpostBaremetal
|
|
3922
|
+
*/
|
|
3923
|
+
loadPackerMaasImageBuildWorkflows() {
|
|
3924
|
+
if (this._packerMaasImageBuildWorkflows) return this._packerMaasImageBuildWorkflows;
|
|
3925
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
3926
|
+
const configPath = path.resolve(__dirname, '../../baremetal/packer-workflows.json');
|
|
3927
|
+
this._packerMaasImageBuildWorkflows = fs.readJsonSync(configPath);
|
|
3928
|
+
return this._packerMaasImageBuildWorkflows;
|
|
3929
|
+
},
|
|
3930
|
+
|
|
3931
|
+
/**
|
|
3932
|
+
* Write Packer MAAS image build workflows configuration to file
|
|
3933
|
+
* @param {object} workflows - The workflows configuration object
|
|
3934
|
+
* @description Writes the Packer MAAS image build workflows to packer-workflows.json
|
|
3935
|
+
* @memberof UnderpostBaremetal
|
|
3936
|
+
*/
|
|
3937
|
+
writePackerMaasImageBuildWorkflows(workflows) {
|
|
3938
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
3939
|
+
const configPath = path.resolve(__dirname, '../../baremetal/packer-workflows.json');
|
|
3940
|
+
fs.writeJsonSync(configPath, workflows, { spaces: 2 });
|
|
3941
|
+
this._packerMaasImageBuildWorkflows = workflows;
|
|
3942
|
+
return configPath;
|
|
3943
|
+
},
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
export default UnderpostBaremetal;
|