mirror of
https://github.com/community-scripts/ProxmoxVE-Local.git
synced 2026-07-22 05:33:03 -04:00
* Add GeneratorTab and UI refactor/styles
Introduce a full-featured GeneratorTab component for building and exporting script configuration (script selector, CPU/RAM/Disk sliders, advanced networking/features, generate/copy/export/import, and execute). Apply visual and structural UI refactors: new glass-card styles across ScriptCard, ScriptCardList, ScriptDetailModal, Terminal and Footer; update button variants and interaction classes; add Heart icon and extra footer links; update layout to use Manrope + JetBrains Mono fonts, sticky navbar, ambient backgrounds and metadata tweaks. Wire GeneratorTab into the main page and add related icon imports. Misc: small code-style/formatting adjustments and improved deriveScriptPath / handler signatures for clarity.
* Add script notes, server presets & APT proxy
Add persistent script notes and server presets, plus APT proxy settings and various UI/behavior improvements.
- DB: new migration adds script_notes and server_presets tables and indexes; Prisma schema updated with ScriptNote and ServerPreset models.
- Notes: new client component ScriptNotesPanel with CRUD using a new TRPC router (scriptNotes) and integration into ScriptDetailModal to show/manage private/shared notes.
- APT proxy: new Next.js API route /api/settings/apt-proxy that reads/writes .env vars; GeneralSettingsModal and ConfigurationModal load/save the proxy and pre-fill advanced install vars.
- UX/UI: add copyable install command, version badges on script cards/detail, container ID input in advanced configuration, and styling tweaks (glass-card-static).
- Server/script detection: installedScripts router now attempts to resolve the actual Proxmox node name via SSH and relaxes ID parsing to numeric-only.
- Notifications: appriseService extended to support Gotify endpoints and allow gotify:// scheme, and URL validation adjusted accordingly.
These changes enable user-maintained notes/presets, persistent APT-cacher defaults, improved install UX, better server detection, and additional notification backend support.
* Add server presets and silent batch updates
Introduce server presets and silent batch update support across API, server, and UI. Added a new serverPresets TRPC router and wired it into the API root. ConfigurationModal now lets users save/load/delete presets tied to a server. InstalledScriptsTab adds a "Silent update" option, a sequential "Update All Containers" batch flow that runs updates with PHS_SILENT=1, and passes envVars into update executions. ScriptExecutionHandler (server.js) now accepts envVars for updates and injects export commands into both local and SSH update flows so environment flags (e.g. PHS_SILENT) are honored. Also added "updated" sort handling in ScriptsGrid, DownloadedScriptsTab and FilterBar, and small typing fixes in appriseService.
* Use per-router Prisma client with Better SQLite3
Replace usage of the shared prisma import with per-router PrismaClient instances configured with @prisma/adapter-better-sqlite3. Add getNotesDb/getPresetsDb helpers (with DATABASE_URL fallback) and update scriptNotes and serverPresets to use the new client. Also add error handling in read endpoints to return empty arrays on DB failures and ensure create/update/delete use the new client.
* Icon-only action buttons; use presets DB
Convert HelpButton, ResyncButton, ServerSettingsButton and SettingsButton from labeled outline controls to compact ghost icon-only buttons with aria-labels and smaller icons/spinners for a more compact header UI. Simplify ResyncButton markup (smaller spinner/SVG, removed extra contextual text/lastSync layout) and tweak sync message styling. Add Server icon import in ServerSettingsButton. In serverPresets router, replace direct prisma calls with getPresetsDb() and use the returned db instance for all queries and mutations to support the presets DB connection.
* Add quick filters, Dev/ARM badges, and CSS tweaks
Introduce quick filter support and visual indicators for developer/ARM scripts. Added QuickFilter type and quickFilter to FilterState (getDefaultFilters/mergeFiltersWithDefaults) and a quick-filter UI in FilterBar (All, New, Updated, In Dev, ARM). Implemented filtering logic in ScriptsGrid and added category dev counts to surface per-category "dev" counts in CategorySidebar. Added DevBadge and ArmBadge components, used in ScriptCard and ScriptDetailModal; ScriptCard also gets a subtle violet highlight when script.is_dev is true. Fixed script path generation in GeneratorTab to include a `scripts/` prefix. Minor dark-theme adjustments to .glass-card/.glass-card-static background and border-color for improved contrast.
* Add appearance settings and persist UI prefs
Introduce an Appearance tab in the Settings modal to control theme, text size and layout width (default vs wide). Preferences are saved to localStorage and applied immediately via helper functions (applyTextSize/applyLayoutWidth) and a small inline script injected into <head> to apply saved settings on first paint. Add CSS utility classes for text-size variants and wire up theme buttons (using lucide icons). Also include minor cleanup: optional chaining fix in CategorySidebar and removal of an unused import in ResyncButton.
* Normalize quotes and format components
Apply consistent formatting across several components: convert single quotes to double quotes, reformat the QuickFilter union type and quick-filters array in FilterBar, adjust JSX prop and className string formatting in HelpButton, ScriptCard, ServerSettingsButton, and SettingsButton. These are cosmetic/formatting changes only and do not alter runtime behavior.
* Refactor Badge & CategorySidebar types/styles
Normalize formatting and strengthen typings for Badge; convert single quotes to double, expand variant/type unions, and tidy JSX/props and helper badge components. Rework CategorySidebar: refactor CategoryIcon SVGs for readability, standardize className usage, improve collapsed-state layout and buttons, and ensure categories are filtered/sorted by count with correct icon mapping and counts display. Overall cleanup improves consistency, readability, and type safety across these components.
* Add modal stacking and portal support
Introduce a modal stacking system and portal to ensure modals escape parent stacking contexts. ModalStackProvider now computes a zIndex for each registered modal and returns an unregister handle; useRegisterModal returns the zIndex. A ModalPortal component (createPortal to document.body) was added and all modal components were updated to: capture the returned zIndex, wrap their markup in <ModalPortal>, and apply the dynamic zIndex instead of a hardcoded z-50 class. This improves correct layering of multiple modals and avoids backdrop-filter / transform stacking issues.
* Wrap modals in portal and use zIndex
Use ModalPortal and zIndex values from useRegisterModal across modal components to avoid hardcoded z-50 stacking. Updated ExecutionModeModal, LXCSettingsModal, PublicKeyModal, ReleaseNotesModal, SetupModal, StorageSelectionModal, and TextViewer to import ModalPortal, capture the zIndex returned by useRegisterModal, wrap modal markup in <ModalPortal>, and apply style={{ zIndex }} to backdrops; LXC result overlay uses zIndex + 10 for proper stacking. Also changed LoadingOverlay in VersionDisplay to render with createPortal(document.body) and added the import. These changes centralize stacking behavior and prevent z-index conflicts when multiple modals/overlays are present.
* Use categoryIconColorMap for sidebar icons
Update CategorySidebar.tsx to derive non-selected icon color classes from categoryIconColorMap for both the "template" and per-category icons. Selected icons still use text-primary; fallbacks preserve previous muted and group-hover classes to retain existing styling when a mapping is absent.
* Standardize modal layout and form styles
Refactor modal markup across many components to use a consistent wrapper structure and unified Tailwind utility ordering (moved zIndex to outer container, standardized backdrop and centering). Clean up form layouts: align icons, labels and inputs, normalize spacing, error styling, and button variants (including icon/ghost adjustments). Improve AuthModal and CloneCountInputModal UX (loading/error states, closer placement, validation), and normalize BackupWarningModal and other modal presentations. Update ConfigurationModal advanced UI: reorganize network fields and add/handle extra options (IPv6 static, gateway, MTU, MAC, VLAN, DNS), refine presets save/cancel flow, and apply general formatting/whitespace cleanups.
* Escape env var values; update favicons & logo
Fix shell escaping when building env export commands by escaping backslashes and quotes (prevents broken exported values) in ScriptExecutionHandler (two locations). Update app metadata to point to favicon files under /favicon, add site manifest, and replace the header Package icon with a next/image using the android-chrome PNG (import Image and adjust markup) for consistent asset handling.
* Add favicon set and logo; remove old favicon
Replace legacy root favicon.png with a full favicon set under public/favicon (android-chrome 192/512, apple-touch-icon, favicon-16x16, favicon-32x32, favicon.ico, favicon.png) and add site.webmanifest. Also add public/logo.png. This organizes favicon assets for multiple platforms and adds a webmanifest for PWA/icon declarations.
* Portalize script dropdown and add outside click
Render the script selector dropdown into document.body via createPortal to escape stacking/overflow contexts. Add triggerRef/dropdownRef, compute fixed position from the trigger on open, and attach an outside-click handler to close the dropdown. Also import useRef/useEffect and createPortal; preserve existing search, selection, and reset behaviors while adjusting z-index/positioning.
* Update favicon and logo assets
Replace favicon and logo image files with updated versions to refresh branding. Updated files: public/favicon/android-chrome-192x192.png, public/favicon/android-chrome-512x512.png, public/favicon/apple-touch-icon.png, public/favicon/favicon-16x16.png, public/favicon/favicon-32x32.png, public/favicon/favicon.ico, public/logo.png.
* Add appearance modal & improve script detail UI
Add an AppearanceModal and AppearanceButton to let users change theme, text size and layout width (persisted in localStorage). Enhance FilterBar with category filtering (selectedCategory, dropdown UI, counts, and outside-click handling). Heavily refactor ScriptDetailModal: layout and visual refresh, new icons, better loading/update/delete/install flows, memoized install command, improved copy behavior, keyboard navigation between scripts (using orderedSlugs + onSelectSlug), clearer status/messages, and various performance/UX tweaks. Wire DownloadedScriptsTab to pass orderedSlugs and onSelectSlug into the ScriptDetailModal. Overall small UI/UX and state-management improvements across components.
* Optimize rendering, lazy-load tabs, add sync modal
Performance and UX improvements: memoize computed values in InstalledScriptsTab (scriptsWithStatus, filteredScripts, uniqueServers) and use useCallback/useMemo in ScriptsGrid to avoid unnecessary re-renders. Memoize ScriptCard and ScriptCardList components and tighten their ScriptCard type alias. Introduce a new SyncModal (ResyncButton) component to run/resync scripts with a progress UI and retry/close behavior, and replace the previous ResyncButton import in page.tsx. Lazy-load heavy tab components (Downloaded, Installed, Backups, Generator) with next/dynamic and add a TabSkeleton loader; consolidate tab definitions into a memoized tabs array. Misc: small prop/prop-name fixes and minor JSX formatting adjustments.
* Add Arcane script, status UI, validation & cache
Multiple changes improving UX, reliability, and CI permissions:
- Add scripts/tools/addon/arcane.sh: installer/update/uninstall helper for Arcane (Docker Compose), creates update helper and generates secrets.
- Add ServerStatusIndicator component and integrate into header (src/app/_components/ServerStatusIndicator.tsx, src/app/page.tsx); adjust hero/layout to accommodate inline version + status.
- Enhance GeneratorTab (src/app/_components/GeneratorTab.tsx): fetch full script details by slug, derive and apply default resource values from install_methods, and show an App Defaults info panel.
- Harden repo provider detection (src/server/lib/repositoryUrlValidation.js/.ts): use URL parsing for hostname matching with safe fallback to 'custom'.
- Tune client caching (src/trpc/query-client.ts): increase staleTime to 30min, set gcTime to 1h, and disable automatic refetches on mount/window focus/reconnect.
- Update GitHub Actions permissions: node.js.yml grants contents: read; release-drafter.yml grants contents: write and pull-requests: read.
These changes aim to provide better default resource handling for scripts, visible server reachability, more robust URL parsing, improved client-side cache behavior, and explicit workflow permissions.
* Add suppressHydrationWarning to <html>
Add the React prop suppressHydrationWarning to the <html> element in RootLayout (src/app/layout.tsx) to suppress hydration mismatch warnings. This helps avoid noisy console warnings when server-rendered markup and client rendering differ (e.g., due to dynamic font class injection or other runtime-only differences).
* Refactor GeneratorTab layout and add SSH imports
Rework GeneratorTab rendering and small UI/format fixes: tidy the scriptDetail useMemo formatting and restructure the Script Defaults block so defaults (CPU, RAM, HDD) are always shown and OS/version/variant badges render correctly. Minor formatting change to ServerStatusIndicator's fetch call and a small spacing fix on the home page title. Also add imports for getSSHService and the Server type to servers router in preparation for SSH-related functionality.
* Add explicit typing for servers in indicator
Annotate the `servers` variable with an explicit array type (id, name, ip, online) and cast `data?.servers` to that type, defaulting to an empty array. This clarifies TypeScript inference and prevents `servers` from being undefined for downstream logic without changing runtime behavior.
* perf/cleanup: audit fixes - hoist iconMap, pin Next.js, strip console.log, lighten glass-card, fix dev badge
* Refactor icons, remove debug logs, pin Next
Replace the large inline SVG icon map with a compact iconPaths mapping and fallback path, simplifying CategoryIcon rendering. Tighten the dev-count badge markup/styles for better layout. Remove leftover console.debug/log statements from InstalledScriptsTab and LXCSettingsModal. Pin Next.js version to 16.2.1 in package.json (deps and devDeps). Slightly adjust dark glass-card background opacity/hover values in globals.css.
* refactor: migrate server TS files to structured logger, accessibility improvements
* perf: fix 14s page load - remove SSH from initial query, defer tab-specific data
- getAllInstalledScripts: removed SSH batchDetectContainerTypes, use DB-only
heuristic (lxc_config presence) for VM/LXC detection
- Defer installedScripts + backups queries until their tab is activated
- Initial batch now only fires 3 fast DB queries instead of 7 (incl. SSH)
* perf: split SSH queries from tRPC batch, skip save-on-mount
- Use splitLink to route servers.checkServersStatus through a separate
non-batched httpLink. SSH queries no longer block fast DB queries from
returning (was causing 14s+ batch response holding all data hostage).
- Add initialization refs in ScriptsGrid and DownloadedScriptsTab so
save-filter/view-mode POST effects skip their first fire after load,
eliminating 2-4 unnecessary network requests per tab mount.
* refactor: restore SSH detection for installed scripts, cache badge counts
- Restore batchDetectContainerTypes SSH calls in getAllInstalledScripts
(shows real VM/LXC state, not just DB heuristic)
- Route getAllInstalledScripts through splitLink (non-batched) so SSH
never blocks fast DB queries
- Cache installed/backups badge counts in localStorage so tabs show
last-known counts instantly, updated when fresh data arrives
- Query still deferred until tab is visited (no eager SSH on page load)
* Refactor init checks, footer, and parallel fetch
Make several small refactors and updates across the app:
- Expand single-line early-return checks into multi-line blocks in DownloadedScriptsTab and ScriptsGrid for clarity when skipping initial effect triggers.
- Update Footer links: add separate GitHub (ProxmoxVE-Local) and GitHub (ProxmoxVE) buttons and change the site link to community-scripts.org.
- Minor formatting tweaks in page.tsx (useState initializer and backups ternary) for readability.
- In the scripts API router, fetch script cards and metadata in parallel via Promise.all to reduce latency and return both together.
These changes improve readability and slightly optimize the scripts fetch path.
* Add in-memory PB cache and invalidate on resync
Introduce a simple server-side in-memory cache for PocketBase data with a 10-minute TTL to reduce PB requests for rarely-changing data. Adds getCached/setCache helpers and a shared _cache store, and exports invalidatePbCache() to clear cached entries. Apply caching to getScriptCards, getCategories, and getScriptTypes, and call invalidatePbCache() in the resyncScripts mutation so fresh data is fetched after a resync.
* Update react.tsx
* Show script download flow & per-node server status
GeneratorTab: add detection of locally downloaded scripts (query + mutation), compute a slug set for O(1) lookup, and gate selection/execution on download state. Update script list UI to indicate downloadable items, grayscale/opacity non-downloaded entries, and show a download confirmation modal that triggers loadScript mutation and invalidates cache. Add Loader2 and AlertTriangle icons and disable Execute when the selected script isn't downloaded.
ServerStatusIndicator: change from a single aggregate dot to per-node indicators showing each host name and status (green online with ping animation, red offline). Improve handling of loading vs no-configured-servers states and update tooltip/title text for clarity.
* Improve updater script and minor UI tweaks
Refactor update.sh to make the updater more robust and flexible: add support for specifying a target release (get_release), include tar in dependency checks, strengthen download/extract logic and error logging, improve source-dir detection, and tighten backup/restore, install/build and service start/rollback flows. Also normalize indentation and logging output and add clearer diagnostic messages on failures. Minor UI cleanups in TSX: simplify GeneratorTab conditional formatting and adjust ServerStatusIndicator class ordering.
* Bump deps and add VSCode Next.js prompt setting
Update multiple dependencies to newer patch/minor versions (Prisma, @tanstack/react-query, axios, better-sqlite3, dotenv, lucide-react, next, react, react-dom, vite, vitest tooling, eslint, jsdom, postcss, prettier, prisma, etc.). Replace legacy typescript-eslint package with @typescript-eslint/eslint-plugin and @typescript-eslint/parser. Add .vscode/settings.json to mark WillLuke.nextjs.hasPrompted as true to suppress the Next.js prompt in VS Code.
* Normalize indentation in core scripts
Reformat scripts/core/alpine-install.func and scripts/core/api.func for consistent indentation and whitespace. Changes are formatting-only (no logic changes): converted leading tabs to spaces, aligned multiline blocks, and adjusted a comment reference (error-handler → error_handler) for consistency. Improves readability and maintainability without affecting behavior.
* Show/hide install command UI and service fixes
UI: Add a Terminal icon, show/hide toggle, and display block for the install command in ScriptDetailModal (new showCommand state and button).
Server: installedScripts - default unknown container types to LXC (safe default) and simplify VM/LXC detection comments.
AppriseService: support gotifys:// (HTTPS) and gotify:// formats, select protocol correctly, and update URL regex.
BackupService: include PBS namespace (--ns) when storage.namespace is configured so proxmox-backup-client commands include the namespace.
ScriptDownloader: add fallback to attempt downloading a ct/<slug>.sh when install_methods is empty for CT/LXC scripts, improve returned success/message semantics, better 404-aware error messaging, and check for fallback CT script presence when scanning downloaded files.
Other: add /examples to .gitignore and remove restore.log.
These changes improve robustness for missing install metadata, add HTTPS Gotify support, ensure PBS namespace usage, and improve UX for viewing install commands.
* Prefer local install scripts; add envVars & token
Allow using local copies of helper/install scripts during development and forward environment variables and a GitHub token through the UI and websocket layer.
Changes:
- scripts/core/alpine-install.func & scripts/core/install.func: try to load misc/tools.func from the local scripts directory before falling back to downloading from GitHub; improved error messaging on failure.
- scripts/core/build.func: resolve SCRIPT_DIR, prefer local alpine/install function files and export their contents to FUNCTIONS_FILE_PATH; fall back to remote download if local files are missing; use local install scripts for lxc-attach when available (fallback to remote). Adjusted dev MOTD path to source from /tmp.
- src/app/_components/ConfigurationModal.tsx: added a password input for var_github_token (passed as GITHUB_TOKEN to mitigate API rate limits).
- src/server/api/websocket/handler.ts: websocket handler now accepts an envVars object and forwards it into startScriptExecution so environment variables from the client can be supplied to script runs.
Overall this improves local development workflows and enables passing custom env vars and a GitHub token to scripts.
* Replace *_json with install_methods/notes
Adapt code to the updated PocketBase schema by renaming install_methods_json and notes_json to install_methods and notes across services and routers. Update parsing in pbScripts.ts, mappings in scripts.ts, autoSyncService.js, and localScripts.ts, and adjust scriptDownloader fallback comment/logic to reference the new field. Also add new PBScript metadata fields (github_data, deleted_message, disable_message, last_update_commit) so the PBScript type reflects the extended record shape.
* Add InstallCommandBlock and integrate installer UI
Introduce a full InstallCommandBlock React component (new file) that builds, previews, and copies install commands with support for GitHub/Gitea, Alpine/Advanced modes, ARM flag, resource presets, and dev gating. Integrate it into ScriptDetailModal: compute hasAlpine and installDefaults from script.install_methods, remove the old simple install command UI/handlers, and embed the new component for non-misc scripts. Update ScriptsGrid to allow aborting batch downloads, invalidate downloaded-scripts cache after downloads, and adjust orderedSlugs passed to the modal (prefer newest when no active filters). On the server, make ScriptDownloaderService resolve dev scripts to the ProxmoxVED repository (dev-specific repo path) while preserving explicit repository_url and default repo behavior.
* Fix envStr possibly undefined in InstallCommandBlock
* Redesign Install section: inline node picker, remove old modals, drop My Notes
* Remove bash command display, pass envVars to SSH, inline Terminal below Install
* Fix envVars: always pass mode to skip whiptail dialog; fix terminal clipping
* Remove dangerous source envVar, fix View button gating, clean dead onInstallScript prop chains
* Tidy JSX formatting and remove extra whitespace
Clean up minor JSX formatting and stray blank lines across components. Removed extra blank lines in DownloadedScriptsTab and ScriptsGrid modal props, reformatted the terminal container div in InstallCommandBlock for clearer attribute layout, and simplified conditional JSX in page.tsx to single-line renders. No functional changes intended—only stylistic/formatting adjustments to improve readability.
* refactor: comprehensive code quality improvements
- Move Terminal component into GeneratorTab (self-contained)
- Replace all alert()/confirm() with modal dialogs
- Type server prop as Server instead of any
- Fix deprecated onKeyPress -> onKeyDown
- Memoize stat counters + fix O(n^2) scriptCounts with Set lookups
- Remove dead hasActions function (always returned true)
- Decompose InstalledScriptsTab: extract Stats, Filters, StatusMessage
- Add buildServerFromScript helper (deduplicates 5 identical blocks)
- Fix eslint-disable blanket: targeted disables + void floating promises
* fix: resolve LXCSettingsModal rootfs_size TS error blocking build
- Type configData as Record<string,unknown> so TS union includes rootfs_size
- Include formatter fixes for extracted sub-components
* feat: bootstrap updater - self-updating update engine
Split update system into two files:
- update.sh: Thin bootstrap (~100 lines) that fetches the latest
update-engine.sh from main branch before executing it
- update-engine.sh: Full update logic (moved from old update.sh)
- UPDATER_VERSION: Version tracker for the engine
How it works:
1. User runs 'bash update.sh' (unchanged workflow)
2. Bootstrap checks UPDATER_VERSION on main vs local
3. If different, downloads latest update-engine.sh from main
4. Hands off to update-engine.sh with all arguments
This ensures bugfixes to the update logic reach users automatically,
without requiring a manual intervention or a new app release.
* fix: widen terminal - collapse sidebar to single column when terminal active
- Add onTerminalChange callback to InstallCommandBlock
- ScriptDetailModal switches from 2-col grid to single column when terminal runs
- Sidebar (ACCESS/DETAILS/INSTALL PROFILES) moves below terminal
- Remove max-w-4xl cap from Terminal.tsx so it fills full container width
- Reset terminalActive state when navigating between scripts
* Release prep: bump to 1.0.0-pre1 + add updater
Update project to pre-release 1.0.0-pre1 and add an interactive updater.
- Bump VERSION and package.json version to 1.0.0-pre1.
- Add pre-release-updater.sh: interactive script to list GitHub prereleases, backup current install, download/extract a selected prerelease, install deps, run migrations, build, and restart the service.
- Minor JSX formatting cleanup in ScriptDetailModal.tsx (split long className into multiple lines for readability; no functional change).
* fix: correct INSTALL_DIR to /opt/ProxmoxVE-Local
* fix(modal): resolve z-index race in ModalStackProvider; refactor(installed-scripts): UI overhaul - stats cards, filters, toolbar, table
* feat(installed-scripts): add Restart/Reboot action; fix orphaned cleanup for missing-server records
* fix(cleanup): delete SSH records with server but no container_id (Pass 1.5); feat(backups): add Create Backup via vzdump in BackupsTab
* fix(generator): await refetch after download; add server selection for SSH execution
- Fix download race condition: await getAllDownloadedScripts.refetch() before
closing dialog and selecting script so isScriptDownloaded returns true
- Add Server type import + state + useEffect to fetch /api/servers on mount
- Add server selection pill buttons above Execute (auto-selects single server)
- Execute button label changes to 'Execute on <name>' or 'Execute Locally'
- Pass mode + server to Terminal for correct local vs SSH execution
- Add selectedServer to handleExecute useCallback dependencies
* fix(generator): restore missing setTimeout body (syntax error)
* style: auto-format BackupsTab and GeneratorTab
* feat(pre2): ProxmoxVED toggle, floating shell dialog, settings UI polish
- Settings: remove Theme block (use AppearanceButton instead), restyle tabs
to match main nav pill style
- Settings: add ProxmoxVED / Dev Scripts toggle (default: off); persisted in
localStorage; StorageEvent keeps ScriptsGrid in sync without context
- ScriptsGrid: filter is_dev scripts from Newest carousel and full grid when
ProxmoxVED is disabled; add showDevScripts -> FilterBar prop
- FilterBar: hide 'In Dev' quick-filter pill when ProxmoxVED is disabled
- ShellContext: new React context (open/minimize/restore/close) for shell sessions
- FloatingShell: new floating dialog with minimize-to-pill, maximize, close;
persists across tab switches; renders via createPortal
- InstalledScriptsTab: delegate shell open to useShell() context instead of
local inline terminal; remove inline shell JSX
- page.tsx: wrap Home in ShellProvider, render FloatingShell at page level
* feat: implement execute_in container picker for addon scripts
- Add execute_in?: string[] | null to Script type
- Forward execute_in from PocketBase via pbToScript() mapper
- Add listContainersOnServer tRPC query (pct list + qm list via SSH)
- GeneratorTab: show container/VM picker when script has execute_in flags
- lxc, pbs, pmg -> show LXC picker (pbs/pmg pin matching containers)
- vm -> show VM picker
- Passes CTID env var to the script on execution
- Picker resets on server or script change
* fix(generator): move container-reset useEffect after selectedSlug declaration
* feat: add prerelease update channel toggle + bump version to 1.0.0-pre2
- Add ALLOW_PRERELEASE env var to env.js
- Add /api/settings/prerelease GET/POST route (persists to .env)
- version.ts: getVersionStatus + getLatestRelease use /releases (all) when
ALLOW_PRERELEASE=true, otherwise /releases/latest (stable only)
- GitHubRelease interface: add prerelease boolean field
- GeneralSettingsModal: add Update Channel toggle in General tab
- VERSION: bump to 1.0.0-pre2
- page.tsx: destructure isAuthenticated from useAuth()
- GeneratorTab.tsx: move container-reset useEffect after selectedSlug decl
* fix(page): destructure logout from useAuth
* feat: floating terminal for backup + New Backup dialog + fixes
- FloatingShell: support backup task mode (isBackup Terminal when
session.backupStorage is set); adds HardDrive icon, onComplete callback
- ShellContext: extend ShellSession with backupStorage, title, onComplete
- BackupsTab: replace blocking createBackupMutation with FloatingShell backup;
add 'New Backup' dialog (server -> container -> storage selection)
- InstalledScriptsTab: fix fetchStorages to not show blocking error modal
- VERSION: bump to 1.0.0-pre3, fixed UTF-8 BOM encoding
* fix: keep Terminal mounted on minimize, add drag-to-move support
- FloatingShell is now a single always-mounted window instead of
conditional branches — visibility:hidden on minimize keeps the
xterm.js WebSocket alive so 'starting shell session' no longer
appears on restore
- Header acts as drag handle: mouse-drag repositions the floating
window freely so the background remains usable
- Maximize still covers full viewport; restore returns to last
dragged position (or CSS-centered if never dragged)
* fix: Server.host -> Server.ip in BackupsTab new backup dialog
* fix: use lxc/vm fields from listContainersOnServer (no containers/vmid)
* fix: bundle core.func+error-handler.func into FUNCTIONS_FILE_PATH
* fix: typed container list in New Backup dialog (no unsafe any)
* feat: v1.0.0-pre4 - multi-session shell, multi-LXC backup, auto-discover after backup, local generator command, addon execute_in
* feat: add Refresh button to Downloaded Scripts tab
* fix: add executeInContainer to WebSocketMessage typedef
* fix: installationId typedef + verbose logo error logging
* fix: add JSDoc type annotations to implicit any callback params
* fix: guard against undefined results[j] in logo cache loop
* Refactor Backups modal and minor UI cleanups
Refactor the Backups create dialog for clarity and better state handling: reorganized JSX, normalized formatting, extract typed container lists, and ensure closing the dialog clears selected storage. Adjust server/container/storage selection flows and button behaviors to improve readability and UX. Also apply small readability/formatting fixes in DownloadedScriptsTab (expand onClick), FloatingShell (simplify Math.min expression), and GeneratorTab (wrap ternary for containerId). These are mostly non-functional cleanup and UI clarity changes across the mentioned components.
* feat: execute_in container routing + install UI overhaul
- GeneratorTab: remove && !!selectedServer from execInContainer so addon
scripts work in local mode (pct exec works without SSH)
- InstallCommandBlock: add executeIn prop + container picker via tRPC
listContainersOnServer; pass executeInContainer/containerId/containerType
to Terminal so addon scripts run inside the container, not on the host
- InstallCommandBlock: remove GitHub/Gitea source toggle; promote
My Defaults + App Defaults to top-level tabs alongside Default/Alpine/Advanced
- ScriptDetailModal: pass execute_in to InstallCommandBlock; add Runs In
badges to Details panel
* fix: GitHub PAT not applied at runtime + light mode contrast
- route.ts: also set process.env.GITHUB_TOKEN in memory after writing to
.env file - tokens saved via UI now take effect immediately without restart
- github.ts: read process.env.GITHUB_TOKEN directly instead of the frozen
t3-env snapshot (env.GITHUB_TOKEN is captured once at startup)
- github.ts: switch Authorization to 'Bearer' scheme (works for both classic
ghp_ and fine-grained github_pat_ tokens per GitHub docs)
- GeneralSettingsModal: fix text-success-foreground / text-error-foreground
-> text-success / text-error so messages are readable in light mode
(success-foreground is white, which is invisible on bg-success/10)
- GeneralSettingsModal: update PAT description to mention both token types
* fix: unblock pre-release updater TypeScript build + cut pre5
- InstallCommandBlock: fix trpc input type for listContainersOnServer
(serverId now numeric fallback instead of string)
- InstallCommandBlock: align container list mapping to API response shape
(use id/name instead of vmid)
- InstallCommandBlock: narrow running.containerType type to 'lxc' | 'vm'
- VERSION: bump to 1.0.0-pre5
- package.json: bump app version to 1.0.0-pre5 (installer/build logs)
This resolves the pre4 updater build failures reported in PR comments.
* feat: backup dialog UX + generator local-node workflow fixes
Backups:
- render create-backup dialog in portal with full-screen backdrop
- fix multi-select flow: explicit steps (server -> containers -> storage)
- keep container selection until user clicks Continue
- show storage free capacity (GB) from pvesm status
- show estimated max backup size from selected container disk templates
Generator:
- remove 'This machine (local)' execution option
- require node selection; execute over SSH only
- generated command now always uses local script path (scripts/...)
instead of remote curl command
- add install mode tabs: Default / My Defaults / App Defaults / Advanced
- apply mode to command/envVars (mode=default|mydefaults|appdefaults|generated)
- use selected container resources as template defaults (cpu/ram/disk)
API:
- getBackupStorages now enriches storages with available/used/total GB
- add getContainersResourceTemplates for per-container cpu/ram/disk templates
* Add advanced options to generator tab
Expose many new advanced/container, network and feature settings in GeneratorTab: password, tags, timezone, container/template storage, protection, IPv6 method/ip/gateway, search domain, nameserver, TUN, keyctl, mknod, verbose, APT cacher (with IP), mount filesystems and SSH authorized key. Wire these into command overrides, env-vars generation, export/import and reset logic. UI updated with grouped sections, IPv6 selector buttons, new inputs and toggles, and FieldInput now supports input type and hint. Also minor refactor to templateDefaults lookup formatting.
* Format JSX in BackupsTab and GeneralSettingsModal
Apply code style/formatting updates across BackupsTab.tsx and GeneralSettingsModal.tsx: reflow JSX props and elements, add consistent line breaks/commas and minor spacing adjustments (including a spacing fix in the GitHub token text). No logic or behavioral changes.
* feat: filter addon/pve types from sync, display, and script loading
- Add UNSUPPORTED_TYPES constant ['addon', 'pve'] in scripts router
- Filter these types from getScriptCards, getScriptCardsWithCategories
- Block loadScript and loadMultipleScripts for unsupported types
- Remove addon/pve filter options from FilterBar UI
- Remove unused Wrench/Server icon imports from FilterBar
* chore: bump version to 1.0.0-pre6
* fix: add pbs_username to hand-written PBSStorageCredential type
* chore: update dependencies and regenerate lockfile
* feat: hide dev count badge in category sidebar when dev mode is off
* feat: add DHCP/STATIC IPv4 mode pills to generator, show IP/gateway fields only when static
* Fix CategorySidebar JSX and format GeneratorTab
Close a mismatched <span> in CategorySidebar so the inner "dev" badge is properly nested and rendered. Also reformat several single-line conditionals and adjust className ordering/formatting in GeneratorTab for improved readability; these are stylistic changes with no functional behavior changes.
* fix: keep floating shell windows mounted when minimized to preserve session state
Instead of unmounting FloatingShellWindow (which destroyed xterm + WebSocket),
all session windows are now kept in the DOM and hidden via CSS (display: none)
when minimized. This preserves the terminal session, history, and WebSocket
connection across minimize/restore cycles.
* feat: add floating shell access to Scripts, Downloaded, and Detail modal
Shell buttons now appear everywhere an installed container is recognized:
- ScriptDetailModal sidebar: 'Containers' section shows each matching
installed container with a direct Shell button (covers Scripts +
Downloaded tabs via the modal)
- ScriptsGrid / ScriptCard: shell button appears on cards (card and list
view) for scripts that have an installed container
- DownloadedScriptsTab: same shell button on cards and list rows
- ScriptCardList: shell button in the header row alongside website link
Matching is done by normalizing the installed script_name against the
card slug (lowercase, strip extension, replace non-alphanumeric with dash).
Only containers with a valid container_id and non-failed status are shown.
* fix: restore downloaded addon/pve scripts visibility in Downloaded tab
The UNSUPPORTED_TYPES filter was applied at the router level in
getScriptCardsWithCategories and getScriptCards. Since DownloadedScriptsTab
builds its list by cross-referencing local files against the GitHub cards
from this endpoint, addon/pve scripts that were already downloaded locally
were invisibly dropped.
Fix: remove the filter from the router, apply it client-side in
ScriptsGrid's combinedScripts memo instead. This means:
- Scripts grid: still hides addon/pve (can't install them)
- Downloaded tab: shows ALL locally downloaded scripts regardless of type
- loadScript / loadMultipleScripts: still block new addon/pve downloads
* Format imports and object indentation
Reformat code in DownloadedScriptsTab.tsx and ScriptDetailModal.tsx for readability: split the React import across multiple lines, adjust placement/indentation of eslint-disable comments, and reflow object property lines in ScriptDetailModal. These are stylistic changes only and do not modify runtime behavior.
* feat: v1.0.0-pre7 - updater channel fix, configurable detection tag, static IP guard
Fixes:
- fix: updater no longer downgrades prerelease installs to stable
version.ts executeUpdate now uses the v1.0.0 branch when VERSION
contains 'pre', so prerelease users always get the prerelease update.sh
- fix: static IP guard in build.func - if NET is the literal word 'static'
(no CIDR entered), fall back to dhcp with a warning instead of creating
a container with ip=static
- fix: ConfigurationModal no longer sends var_net=static to the script
when no IP address was entered in the static IP field
Features:
- feat: configurable container detection tag (Settings > General)
Users can override 'community-script' with any tag (e.g. 'cs')
Stored in .env as CONTAINER_DETECTION_TAG, used by autoDetectLXCContainers
Version:
- bump VERSION + package.json to 1.0.0-pre7
* fix: preserve downloaded scripts across updates
- pre-release updater now backs up/restores scripts ct/tools/vm/vw
- prevent rsync from overwriting downloaded script directories
- include scripts/vw in update-engine backup/restore/rollback/excludes
This prevents downloaded scripts from disappearing after update and
explains why only bundled defaults (e.g. debian + arcane) remained.
* feat(ui): unify floating terminals and enable addon execution
* chore: bump version to 1.0.0-pre8
* fix(updater): skip engine self-update gracefully when UPDATER_VERSION not on main (pre-release)
* fix: SSH interactive input, suppress rsync output, 4-theme picker, sort containers by ID
* fix(ui): overhaul terminal themes with strong contrast and real preview cards
* feat(pre9): execute_in policy matrix, full type visibility, lxc-only generator
* fix(pre9): make VM/LXC status and type detection server-scoped
---------
Co-authored-by: MickLesk <mickey.leskowitz@levelbuild.com>
6086 lines
220 KiB
Bash
Executable File
6086 lines
220 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Copyright (c) 2021-2026 community-scripts ORG
|
||
# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
|
||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE
|
||
|
||
# ==============================================================================
|
||
# BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION
|
||
# ==============================================================================
|
||
#
|
||
# This file provides the main build functions for creating and configuring
|
||
# LXC containers in Proxmox VE. It handles:
|
||
#
|
||
# - Variable initialization and defaults
|
||
# - Container creation and resource allocation
|
||
# - Storage selection and management
|
||
# - Advanced configuration and customization
|
||
# - User interaction menus and prompts
|
||
#
|
||
# Usage:
|
||
# - Sourced automatically by CT creation scripts
|
||
# - Requires core.func and error_handler.func to be loaded first
|
||
#
|
||
# ==============================================================================
|
||
|
||
# ==============================================================================
|
||
# SECTION 1: INITIALIZATION & CORE VARIABLES
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# variables()
|
||
#
|
||
# - Initializes core variables for container creation
|
||
# - Normalizes application name (NSAPP = lowercase, no spaces)
|
||
# - Builds installer filename (var_install)
|
||
# - Defines regex patterns for validation
|
||
# - Fetches Proxmox hostname and version
|
||
# - Generates unique session ID for tracking and logging
|
||
# - Captures app-declared resource defaults (CPU, RAM, Disk)
|
||
# ------------------------------------------------------------------------------
|
||
variables() {
|
||
NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
|
||
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
|
||
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
|
||
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
|
||
DIAGNOSTICS="no" # Safe default: no telemetry until user consents via diagnostics_check()
|
||
METHOD="default" # sets the METHOD variable to "default", used for the API call.
|
||
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
|
||
EXECUTION_ID="${RANDOM_UUID}" # Unique execution ID for telemetry record identification (unique-indexed in PocketBase)
|
||
SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files
|
||
BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log
|
||
# NOTE: combined_log is constructed locally in build_container() and ensure_log_on_host()
|
||
# as "/tmp/${NSAPP}-${CTID}-${SESSION_ID}.log" (requires CTID, not available here)
|
||
CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
|
||
|
||
# Parse dev_mode early
|
||
parse_dev_mode
|
||
|
||
# Setup persistent log directory if logs mode active
|
||
if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then
|
||
mkdir -p /var/log/community-scripts
|
||
BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log"
|
||
fi
|
||
|
||
# Get Proxmox VE version and kernel version
|
||
if command -v pveversion >/dev/null 2>&1; then
|
||
PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
|
||
else
|
||
PVEVERSION="N/A"
|
||
fi
|
||
KERNEL_VERSION=$(uname -r)
|
||
|
||
# Capture app-declared defaults (for precedence logic)
|
||
# These values are set by the app script BEFORE default.vars is loaded
|
||
# If app declares higher values than default.vars, app values take precedence
|
||
if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then
|
||
export APP_DEFAULT_CPU="${var_cpu}"
|
||
fi
|
||
if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then
|
||
export APP_DEFAULT_RAM="${var_ram}"
|
||
fi
|
||
if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then
|
||
export APP_DEFAULT_DISK="${var_disk}"
|
||
fi
|
||
}
|
||
|
||
source "$(dirname "${BASH_SOURCE[0]}")/api.func"
|
||
|
||
if command -v curl >/dev/null 2>&1; then
|
||
source "$(dirname "${BASH_SOURCE[0]}")/core.func"
|
||
source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func"
|
||
load_functions
|
||
catch_errors
|
||
elif command -v wget >/dev/null 2>&1; then
|
||
source "$(dirname "${BASH_SOURCE[0]}")/core.func"
|
||
source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func"
|
||
load_functions
|
||
catch_errors
|
||
fi
|
||
|
||
# ==============================================================================
|
||
# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# maxkeys_check()
|
||
#
|
||
# - Reads kernel keyring limits (maxkeys, maxbytes)
|
||
# - Checks current usage for LXC user (UID 100000)
|
||
# - Warns if usage is close to limits and suggests sysctl tuning
|
||
# - Exits if thresholds are exceeded
|
||
# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
|
||
# ------------------------------------------------------------------------------
|
||
|
||
maxkeys_check() {
|
||
# Read kernel parameters
|
||
per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
|
||
per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
|
||
|
||
# Exit if kernel parameters are unavailable
|
||
if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
|
||
msg_error "Unable to read kernel key parameters. Ensure proper permissions."
|
||
exit 107
|
||
fi
|
||
|
||
# Fetch key usage for user ID 100000 (typical for containers)
|
||
used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
|
||
used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
|
||
|
||
# Calculate thresholds and suggested new limits
|
||
threshold_keys=$((per_user_maxkeys - 100))
|
||
threshold_bytes=$((per_user_maxbytes - 1000))
|
||
new_limit_keys=$((per_user_maxkeys * 2))
|
||
new_limit_bytes=$((per_user_maxbytes * 2))
|
||
|
||
# Check if key or byte usage is near limits
|
||
failure=0
|
||
if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
|
||
msg_warn "Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys})"
|
||
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
|
||
failure=1
|
||
fi
|
||
if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
|
||
msg_warn "Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes})"
|
||
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
|
||
failure=1
|
||
fi
|
||
|
||
# Provide next steps if issues are detected
|
||
if [[ "$failure" -eq 1 ]]; then
|
||
msg_error "Kernel key limits exceeded - see suggestions above"
|
||
exit 108
|
||
fi
|
||
|
||
# Silent success - only show errors if they exist
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 3: CONTAINER SETUP UTILITIES
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_current_ip()
|
||
#
|
||
# - Returns current container IP depending on OS type
|
||
# - Debian/Ubuntu: uses `hostname -I`
|
||
# - Alpine: parses eth0 via `ip -4 addr` or `ip -6 addr`
|
||
# - Supports IPv6-only environments as fallback
|
||
# - Returns "Unknown" if OS type cannot be determined
|
||
# ------------------------------------------------------------------------------
|
||
get_current_ip() {
|
||
CURRENT_IP=""
|
||
if [ -f /etc/os-release ]; then
|
||
# Check for Debian/Ubuntu (uses hostname -I)
|
||
if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
|
||
# Try IPv4 first
|
||
CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)
|
||
# Fallback to IPv6 if no IPv4
|
||
if [[ -z "$CURRENT_IP" ]]; then
|
||
CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1 || true)
|
||
fi
|
||
# Check for Alpine (uses ip command)
|
||
elif grep -q 'ID=alpine' /etc/os-release; then
|
||
# Try IPv4 first
|
||
CURRENT_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
|
||
# Fallback to IPv6 if no IPv4
|
||
if [[ -z "$CURRENT_IP" ]]; then
|
||
CURRENT_IP=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n 1)
|
||
fi
|
||
else
|
||
CURRENT_IP="Unknown"
|
||
fi
|
||
fi
|
||
echo "$CURRENT_IP"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# update_motd_ip()
|
||
#
|
||
# - Updates /etc/motd with current container IP
|
||
# - Removes old IP entries to avoid duplicates
|
||
# - Regenerates /etc/profile.d/00_lxc-details.sh with dynamic OS/IP info
|
||
# ------------------------------------------------------------------------------
|
||
update_motd_ip() {
|
||
MOTD_FILE="/etc/motd"
|
||
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
|
||
|
||
if [ -f "$MOTD_FILE" ]; then
|
||
# Remove existing IP Address lines to prevent duplication
|
||
sed -i '/IP Address:/d' "$MOTD_FILE"
|
||
|
||
IP=$(get_current_ip)
|
||
# Add the new IP address
|
||
echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
|
||
fi
|
||
|
||
# Update dynamic LXC details profile if values changed (e.g., after OS upgrade)
|
||
# Only update if file exists and is from community-scripts
|
||
if [ -f "$PROFILE_FILE" ] && grep -q "community-scripts" "$PROFILE_FILE" 2>/dev/null; then
|
||
# Get current values
|
||
local current_os="$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - Version: $(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')"
|
||
local current_hostname="$(hostname)"
|
||
local current_ip="$(hostname -I | awk '{print $1}')"
|
||
|
||
# Escape sed special chars in replacement strings (& \ |)
|
||
current_os="${current_os//\\/\\\\}"
|
||
current_os="${current_os//&/\\&}"
|
||
current_hostname="${current_hostname//\\/\\\\}"
|
||
current_hostname="${current_hostname//&/\\&}"
|
||
current_ip="${current_ip//\\/\\\\}"
|
||
current_ip="${current_ip//&/\\&}"
|
||
|
||
# Update only if values actually changed
|
||
if ! grep -q "OS:.*$current_os" "$PROFILE_FILE" 2>/dev/null; then
|
||
sed -i "s|OS:.*|OS: \${GN}$current_os\${CL}\\\"|" "$PROFILE_FILE"
|
||
fi
|
||
if ! grep -q "Hostname:.*$current_hostname" "$PROFILE_FILE" 2>/dev/null; then
|
||
sed -i "s|Hostname:.*|Hostname: \${GN}$current_hostname\${CL}\\\"|" "$PROFILE_FILE"
|
||
fi
|
||
if ! grep -q "IP Address:.*$current_ip" "$PROFILE_FILE" 2>/dev/null; then
|
||
sed -i "s|IP Address:.*|IP Address: \${GN}$current_ip\${CL}\\\"|" "$PROFILE_FILE"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# install_ssh_keys_into_ct()
|
||
#
|
||
# - Installs SSH keys into container root account if SSH is enabled
|
||
# - Uses pct push or direct input to authorized_keys
|
||
# - Supports both SSH_KEYS_FILE (from advanced settings) and SSH_AUTHORIZED_KEY (from user defaults)
|
||
# - Falls back to warning if no keys provided
|
||
# ------------------------------------------------------------------------------
|
||
install_ssh_keys_into_ct() {
|
||
[[ "${SSH:-no}" != "yes" ]] && return 0
|
||
|
||
# Ensure SSH_KEYS_FILE is defined (may not be set if advanced_settings was skipped)
|
||
: "${SSH_KEYS_FILE:=}"
|
||
|
||
# If SSH_KEYS_FILE doesn't exist but SSH_AUTHORIZED_KEY is set (from user defaults),
|
||
# create a temporary SSH_KEYS_FILE with the key
|
||
if [[ -z "$SSH_KEYS_FILE" || ! -s "$SSH_KEYS_FILE" ]] && [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then
|
||
SSH_KEYS_FILE="$(mktemp)"
|
||
printf '%s\n' "$SSH_AUTHORIZED_KEY" >"$SSH_KEYS_FILE"
|
||
fi
|
||
|
||
if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
|
||
msg_info "Installing selected SSH keys into CT ${CTID}"
|
||
pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || {
|
||
msg_error "prepare /root/.ssh failed"
|
||
return 252
|
||
}
|
||
pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 ||
|
||
pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || {
|
||
msg_error "write authorized_keys failed"
|
||
return 252
|
||
}
|
||
pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
|
||
msg_ok "Installed SSH keys into CT ${CTID}"
|
||
return 0
|
||
fi
|
||
|
||
# Fallback
|
||
msg_warn "No SSH keys to install (skipping)."
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_container_id()
|
||
#
|
||
# - Validates if a container ID is available for use (CLUSTER-WIDE)
|
||
# - Checks cluster resources via pvesh for VMs/CTs on ALL nodes
|
||
# - Falls back to local config file check if pvesh unavailable
|
||
# - Checks if ID is used in LVM logical volumes
|
||
# - Returns 0 if ID is available, 1 if already in use
|
||
# ------------------------------------------------------------------------------
|
||
validate_container_id() {
|
||
local ctid="$1"
|
||
|
||
# Check if ID is numeric
|
||
if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# CLUSTER-WIDE CHECK: Query all VMs/CTs across all nodes
|
||
# This catches IDs used on other nodes in the cluster
|
||
# NOTE: Works on single-node too - Proxmox always has internal cluster structure
|
||
# Falls back gracefully if pvesh unavailable or returns empty
|
||
if command -v pvesh &>/dev/null; then
|
||
local cluster_ids
|
||
cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
|
||
grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true)
|
||
if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# LOCAL FALLBACK: Check if config file exists for VM or LXC
|
||
# This handles edge cases where pvesh might not return all info
|
||
if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# Check ALL nodes in cluster for config files (handles pmxcfs sync delays)
|
||
# NOTE: On single-node, /etc/pve/nodes/ contains just the one node - still works
|
||
if [[ -d "/etc/pve/nodes" ]]; then
|
||
for node_dir in /etc/pve/nodes/*/; do
|
||
if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then
|
||
return 1
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Check if ID is used in LVM logical volumes
|
||
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_valid_container_id()
|
||
#
|
||
# - Returns a valid, unused container ID (CLUSTER-AWARE)
|
||
# - Uses pvesh /cluster/nextid as starting point (already cluster-aware)
|
||
# - If provided ID is valid, returns it
|
||
# - Otherwise increments until a free one is found across entire cluster
|
||
# - Calls validate_container_id() to check availability
|
||
# ------------------------------------------------------------------------------
|
||
get_valid_container_id() {
|
||
local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}"
|
||
|
||
# Ensure we have a valid starting ID
|
||
if ! [[ "$suggested_id" =~ ^[0-9]+$ ]]; then
|
||
suggested_id=$(pvesh get /cluster/nextid 2>/dev/null || echo 100)
|
||
fi
|
||
|
||
local max_attempts=1000
|
||
local attempts=0
|
||
|
||
while ! validate_container_id "$suggested_id"; do
|
||
suggested_id=$((suggested_id + 1))
|
||
attempts=$((attempts + 1))
|
||
if [[ $attempts -ge $max_attempts ]]; then
|
||
msg_error "Could not find available container ID after $max_attempts attempts"
|
||
exit 109
|
||
fi
|
||
done
|
||
|
||
echo "$suggested_id"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_hostname()
|
||
#
|
||
# - Validates hostname/FQDN according to RFC 1123/952
|
||
# - Checks total length (max 253 characters for FQDN)
|
||
# - Validates each label (max 63 chars, alphanumeric + hyphens)
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_hostname() {
|
||
local hostname="$1"
|
||
|
||
# Check total length (max 253 for FQDN)
|
||
if [[ ${#hostname} -gt 253 ]] || [[ -z "$hostname" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# Split by dots and validate each label
|
||
local IFS='.'
|
||
read -ra labels <<<"$hostname"
|
||
for label in "${labels[@]}"; do
|
||
# Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end)
|
||
if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then
|
||
return 1
|
||
fi
|
||
if [[ ! "$label" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] && [[ ! "$label" =~ ^[a-z0-9]$ ]]; then
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_mac_address()
|
||
#
|
||
# - Validates MAC address format (XX:XX:XX:XX:XX:XX)
|
||
# - Empty value is allowed (auto-generated)
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_mac_address() {
|
||
local mac="$1"
|
||
[[ -z "$mac" ]] && return 0
|
||
if [[ ! "$mac" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_vlan_tag()
|
||
#
|
||
# - Validates VLAN tag (1-4094)
|
||
# - Empty value is allowed (no VLAN)
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_vlan_tag() {
|
||
local vlan="$1"
|
||
[[ -z "$vlan" ]] && return 0
|
||
if ! [[ "$vlan" =~ ^[0-9]+$ ]] || ((vlan < 1 || vlan > 4094)); then
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_mtu()
|
||
#
|
||
# - Validates MTU size (576-65535, common values: 1500, 9000)
|
||
# - Empty value is allowed (default 1500)
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_mtu() {
|
||
local mtu="$1"
|
||
[[ -z "$mtu" ]] && return 0
|
||
if ! [[ "$mtu" =~ ^[0-9]+$ ]] || ((mtu < 576 || mtu > 65535)); then
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_ipv6_address()
|
||
#
|
||
# - Validates IPv6 address with optional CIDR notation
|
||
# - Supports compressed (::) and full notation
|
||
# - Empty value is allowed
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_ipv6_address() {
|
||
local ipv6="$1"
|
||
[[ -z "$ipv6" ]] && return 0
|
||
|
||
# Extract address and CIDR
|
||
local addr="${ipv6%%/*}"
|
||
local cidr="${ipv6##*/}"
|
||
|
||
# Validate CIDR if present (1-128)
|
||
if [[ "$ipv6" == */* ]]; then
|
||
if ! [[ "$cidr" =~ ^[0-9]+$ ]] || ((cidr < 1 || cidr > 128)); then
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Basic IPv6 validation - check for valid characters and structure
|
||
# Must contain only hex digits and colons
|
||
if [[ ! "$addr" =~ ^[0-9a-fA-F:]+$ ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# Must contain at least one colon
|
||
if [[ ! "$addr" == *:* ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# Check for valid double-colon usage (only one :: allowed)
|
||
if [[ "$addr" == *::*::* ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# Check that no segment exceeds 4 hex chars
|
||
local IFS=':'
|
||
local -a segments
|
||
read -ra segments <<<"$addr"
|
||
for seg in "${segments[@]}"; do
|
||
if [[ ${#seg} -gt 4 ]]; then
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_bridge()
|
||
#
|
||
# - Validates that network bridge exists and is active
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_bridge() {
|
||
local bridge="$1"
|
||
[[ -z "$bridge" ]] && return 1
|
||
|
||
# Check if bridge interface exists
|
||
if ! ip link show "$bridge" &>/dev/null; then
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_gateway_in_subnet()
|
||
#
|
||
# - Validates that gateway IP is in the same subnet as static IP
|
||
# - Arguments: static_ip (with CIDR), gateway_ip
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_gateway_in_subnet() {
|
||
local static_ip="$1"
|
||
local gateway="$2"
|
||
|
||
[[ -z "$static_ip" || -z "$gateway" ]] && return 0
|
||
|
||
# Extract IP and CIDR
|
||
local ip="${static_ip%%/*}"
|
||
local cidr="${static_ip##*/}"
|
||
|
||
# /31 and /32 are valid point-to-point / zero-trust DMZ configurations
|
||
# where the gateway is technically outside the subnet — skip validation
|
||
((cidr >= 31)) && return 0
|
||
|
||
# Convert CIDR to netmask bits
|
||
local mask=$((0xFFFFFFFF << (32 - cidr) & 0xFFFFFFFF))
|
||
|
||
# Convert IPs to integers
|
||
local IFS='.'
|
||
read -r i1 i2 i3 i4 <<<"$ip"
|
||
read -r g1 g2 g3 g4 <<<"$gateway"
|
||
|
||
local ip_int=$(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
|
||
local gw_int=$(((g1 << 24) + (g2 << 16) + (g3 << 8) + g4))
|
||
|
||
# Check if both are in same network
|
||
if (((ip_int & mask) != (gw_int & mask))); then
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_ip_address()
|
||
#
|
||
# - Validates IPv4 address with CIDR notation
|
||
# - Checks each octet is 0-255
|
||
# - Checks CIDR is 1-32
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_ip_address() {
|
||
local ip="$1"
|
||
[[ -z "$ip" ]] && return 1
|
||
|
||
# Check format with CIDR
|
||
if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$ ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local o1="${BASH_REMATCH[1]}"
|
||
local o2="${BASH_REMATCH[2]}"
|
||
local o3="${BASH_REMATCH[3]}"
|
||
local o4="${BASH_REMATCH[4]}"
|
||
local cidr="${BASH_REMATCH[5]}"
|
||
|
||
# Validate octets (0-255)
|
||
for octet in "$o1" "$o2" "$o3" "$o4"; do
|
||
if ((octet > 255)); then
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
# Validate CIDR (1-32)
|
||
if ((cidr < 1 || cidr > 32)); then
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_gateway_ip()
|
||
#
|
||
# - Validates gateway IPv4 address (without CIDR)
|
||
# - Checks each octet is 0-255
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_gateway_ip() {
|
||
local ip="$1"
|
||
[[ -z "$ip" ]] && return 0
|
||
|
||
# Check format without CIDR
|
||
if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
|
||
return 1
|
||
fi
|
||
|
||
local o1="${BASH_REMATCH[1]}"
|
||
local o2="${BASH_REMATCH[2]}"
|
||
local o3="${BASH_REMATCH[3]}"
|
||
local o4="${BASH_REMATCH[4]}"
|
||
|
||
# Validate octets (0-255)
|
||
for octet in "$o1" "$o2" "$o3" "$o4"; do
|
||
if ((octet > 255)); then
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_timezone()
|
||
#
|
||
# - Validates timezone string against system zoneinfo
|
||
# - Empty value or "host" is allowed
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_timezone() {
|
||
local tz="$1"
|
||
[[ -z "$tz" || "$tz" == "host" ]] && return 0
|
||
|
||
# Check if timezone file exists
|
||
if [[ ! -f "/usr/share/zoneinfo/$tz" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_tags()
|
||
#
|
||
# - Validates Proxmox tags format
|
||
# - Only alphanumeric, hyphens, underscores, and semicolons allowed
|
||
# - Empty value is allowed
|
||
# - Returns 0 if valid, 1 if invalid
|
||
# ------------------------------------------------------------------------------
|
||
validate_tags() {
|
||
local tags="$1"
|
||
[[ -z "$tags" ]] && return 0
|
||
|
||
# Tags can only contain alphanumeric, -, _, and ; (separator)
|
||
if [[ ! "$tags" =~ ^[a-zA-Z0-9_\;-]+$ ]]; then
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# find_host_ssh_keys()
|
||
#
|
||
# - Scans system for available SSH keys
|
||
# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
|
||
# - Returns list of files containing valid SSH public keys
|
||
# - Sets FOUND_HOST_KEY_COUNT to number of keys found
|
||
# ------------------------------------------------------------------------------
|
||
find_host_ssh_keys() {
|
||
local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
|
||
local -a files=() cand=()
|
||
local g="${var_ssh_import_glob:-}"
|
||
local total=0 f base c
|
||
|
||
shopt -s nullglob
|
||
if [[ -n "$g" ]]; then
|
||
for pat in $g; do cand+=($pat); done
|
||
else
|
||
cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
|
||
cand+=(/root/.ssh/*.pub)
|
||
cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
|
||
fi
|
||
shopt -u nullglob
|
||
|
||
for f in "${cand[@]}"; do
|
||
[[ -f "$f" && -r "$f" ]] || continue
|
||
base="$(basename -- "$f")"
|
||
case "$base" in
|
||
known_hosts | known_hosts.* | config) continue ;;
|
||
id_*) [[ "$f" != *.pub ]] && continue ;;
|
||
esac
|
||
|
||
# CRLF safe check for host keys
|
||
c=$(tr -d '\r' <"$f" | awk '
|
||
/^[[:space:]]*#/ {next}
|
||
/^[[:space:]]*$/ {next}
|
||
{print}
|
||
' | grep -E -c "$re" || true)
|
||
|
||
if ((c > 0)); then
|
||
files+=("$f")
|
||
total=$((total + c))
|
||
fi
|
||
done
|
||
|
||
# Fallback to /root/.ssh/authorized_keys
|
||
if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
|
||
if grep -E -q "$re" /root/.ssh/authorized_keys; then
|
||
files+=(/root/.ssh/authorized_keys)
|
||
total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
|
||
fi
|
||
fi
|
||
|
||
FOUND_HOST_KEY_COUNT="$total"
|
||
(
|
||
IFS=:
|
||
echo "${files[*]}"
|
||
)
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 3B: IP RANGE SCANNING
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ip_to_int() / int_to_ip()
|
||
#
|
||
# - Converts IP address to integer and vice versa for range iteration
|
||
# ------------------------------------------------------------------------------
|
||
ip_to_int() {
|
||
local IFS=.
|
||
read -r i1 i2 i3 i4 <<<"$1"
|
||
echo $(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
|
||
}
|
||
|
||
int_to_ip() {
|
||
local ip=$1
|
||
echo "$(((ip >> 24) & 0xFF)).$(((ip >> 16) & 0xFF)).$(((ip >> 8) & 0xFF)).$((ip & 0xFF))"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# resolve_ip_from_range()
|
||
#
|
||
# - Takes an IP range in format "10.0.0.1/24-10.0.0.10/24"
|
||
# - Pings each IP in the range to find the first available one
|
||
# - Returns the first free IP with CIDR notation
|
||
# - Sets NET_RESOLVED to the resolved IP or empty on failure
|
||
# ------------------------------------------------------------------------------
|
||
resolve_ip_from_range() {
|
||
local range="$1"
|
||
local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$'
|
||
local ip_start ip_end
|
||
|
||
# Parse range: "10.0.0.1/24-10.0.0.10/24"
|
||
ip_start="${range%%-*}"
|
||
ip_end="${range##*-}"
|
||
|
||
if [[ ! "$ip_start" =~ $ip_cidr_regex ]] || [[ ! "$ip_end" =~ $ip_cidr_regex ]]; then
|
||
NET_RESOLVED=""
|
||
return 1
|
||
fi
|
||
|
||
local ip1="${ip_start%%/*}"
|
||
local ip2="${ip_end%%/*}"
|
||
local cidr="${ip_start##*/}"
|
||
|
||
local start_int=$(ip_to_int "$ip1")
|
||
local end_int=$(ip_to_int "$ip2")
|
||
|
||
for ((ip_int = start_int; ip_int <= end_int; ip_int++)); do
|
||
local ip=$(int_to_ip $ip_int)
|
||
msg_info "Checking IP: $ip"
|
||
if ! ping -c 1 -W 1 "$ip" >/dev/null 2>&1; then
|
||
NET_RESOLVED="$ip/$cidr"
|
||
msg_ok "Found free IP: ${BGN}$NET_RESOLVED${CL}"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
NET_RESOLVED=""
|
||
msg_error "No free IP found in range $range"
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# is_ip_range()
|
||
#
|
||
# - Checks if a string is an IP range (contains - and looks like IP/CIDR)
|
||
# - Returns 0 if it's a range, 1 otherwise
|
||
# ------------------------------------------------------------------------------
|
||
is_ip_range() {
|
||
local value="$1"
|
||
local ip_start ip_end
|
||
if [[ "$value" == *-* ]] && [[ "$value" != "dhcp" ]]; then
|
||
local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$'
|
||
ip_start="${value%%-*}"
|
||
ip_end="${value##*-}"
|
||
if [[ "$ip_start" =~ $ip_cidr_regex ]] && [[ "$ip_end" =~ $ip_cidr_regex ]]; then
|
||
return 0
|
||
fi
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 4: STORAGE & RESOURCE MANAGEMENT
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# _write_storage_to_vars()
|
||
#
|
||
# - Writes storage selection to vars file
|
||
# - Removes old entries (commented and uncommented) to avoid duplicates
|
||
# - Arguments: vars_file, key (var_container_storage/var_template_storage), value
|
||
# ------------------------------------------------------------------------------
|
||
_write_storage_to_vars() {
|
||
# $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
|
||
local vf="$1" key="$2" val="$3"
|
||
# remove uncommented and commented versions to avoid duplicates
|
||
sed -i "/^[#[:space:]]*${key}=/d" "$vf"
|
||
echo "${key}=${val}" >>"$vf"
|
||
}
|
||
|
||
choose_and_set_storage_for_file() {
|
||
# $1 = vars_file, $2 = class ('container'|'template')
|
||
local vf="$1" class="$2" key="" current=""
|
||
case "$class" in
|
||
container) key="var_container_storage" ;;
|
||
template) key="var_template_storage" ;;
|
||
*)
|
||
msg_error "Unknown storage class: $class"
|
||
return 65
|
||
;;
|
||
esac
|
||
|
||
current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")
|
||
|
||
# If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
|
||
local content="rootdir"
|
||
[[ "$class" == "template" ]] && content="vztmpl"
|
||
local count
|
||
count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)
|
||
|
||
if [[ "$count" -eq 1 ]]; then
|
||
STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
|
||
STORAGE_INFO=""
|
||
|
||
# Validate storage space for auto-picked container storage
|
||
if [[ "$class" == "container" && -n "${DISK_SIZE:-}" ]]; then
|
||
validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes"
|
||
# Continue even if validation fails - user was warned
|
||
fi
|
||
else
|
||
# If the current value is preselectable, we could show it, but per your requirement we always offer selection
|
||
select_storage "$class" || return 150
|
||
fi
|
||
|
||
_write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"
|
||
|
||
# Keep environment in sync for later steps (e.g. app-default save)
|
||
if [[ "$class" == "container" ]]; then
|
||
export var_container_storage="$STORAGE_RESULT"
|
||
export CONTAINER_STORAGE="$STORAGE_RESULT"
|
||
else
|
||
export var_template_storage="$STORAGE_RESULT"
|
||
export TEMPLATE_STORAGE="$STORAGE_RESULT"
|
||
fi
|
||
|
||
# Silent operation - no output message
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# base_settings()
|
||
#
|
||
# - Defines all base/default variables for container creation
|
||
# - Reads from environment variables (var_*)
|
||
# - Provides fallback defaults for OS type/version
|
||
# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK)
|
||
# - Sets up container type, resources, network, SSH, features, and tags
|
||
# ------------------------------------------------------------------------------
|
||
base_settings() {
|
||
# Default Settings
|
||
CT_TYPE=${var_unprivileged:-"1"}
|
||
|
||
# Resource allocation: App defaults take precedence if HIGHER
|
||
# Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values
|
||
local final_disk="${var_disk:-4}"
|
||
local final_cpu="${var_cpu:-1}"
|
||
local final_ram="${var_ram:-1024}"
|
||
|
||
# If app declared higher values, use those instead
|
||
if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then
|
||
if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then
|
||
final_disk="${APP_DEFAULT_DISK}"
|
||
fi
|
||
fi
|
||
|
||
if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then
|
||
if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then
|
||
final_cpu="${APP_DEFAULT_CPU}"
|
||
fi
|
||
fi
|
||
|
||
if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then
|
||
if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then
|
||
final_ram="${APP_DEFAULT_RAM}"
|
||
fi
|
||
fi
|
||
|
||
DISK_SIZE="${final_disk}"
|
||
CORE_COUNT="${final_cpu}"
|
||
RAM_SIZE="${final_ram}"
|
||
VERBOSE=${var_verbose:-"${1:-no}"}
|
||
PW=""
|
||
if [[ -n "${var_pw:-}" ]]; then
|
||
local _pw_raw="${var_pw}"
|
||
case "$_pw_raw" in
|
||
--password\ *) _pw_raw="${_pw_raw#--password }" ;;
|
||
-password\ *) _pw_raw="${_pw_raw#-password }" ;;
|
||
esac
|
||
while [[ "$_pw_raw" == -* ]]; do
|
||
_pw_raw="${_pw_raw#-}"
|
||
done
|
||
if [[ -z "$_pw_raw" ]]; then
|
||
msg_warn "Password was only dashes after cleanup; leaving empty."
|
||
else
|
||
PW="--password $_pw_raw"
|
||
fi
|
||
fi
|
||
|
||
# Validate and set Container ID
|
||
local requested_id="${var_ctid:-$NEXTID}"
|
||
if ! validate_container_id "$requested_id"; then
|
||
# Only show warning if user manually specified an ID (not auto-assigned)
|
||
if [[ -n "${var_ctid:-}" ]]; then
|
||
msg_warn "Container ID $requested_id is already in use. Using next available ID: $(get_valid_container_id "$requested_id")"
|
||
fi
|
||
requested_id=$(get_valid_container_id "$requested_id")
|
||
fi
|
||
CT_ID="$requested_id"
|
||
|
||
# Validate and set Hostname/FQDN
|
||
local requested_hostname="${var_hostname:-$NSAPP}"
|
||
requested_hostname=$(echo "${requested_hostname,,}" | tr -d ' ')
|
||
if ! validate_hostname "$requested_hostname"; then
|
||
if [[ -n "${var_hostname:-}" ]]; then
|
||
msg_warn "Invalid hostname '$requested_hostname'. Using default: $NSAPP"
|
||
fi
|
||
requested_hostname="$NSAPP"
|
||
fi
|
||
HN="$requested_hostname"
|
||
|
||
BRG=${var_brg:-"vmbr0"}
|
||
NET=${var_net:-"dhcp"}
|
||
|
||
# Resolve IP range if NET contains a range (e.g., 192.168.1.100/24-192.168.1.200/24)
|
||
if is_ip_range "$NET"; then
|
||
msg_info "Scanning IP range: $NET"
|
||
if resolve_ip_from_range "$NET"; then
|
||
NET="$NET_RESOLVED"
|
||
else
|
||
msg_error "Could not find free IP in range. Falling back to DHCP."
|
||
NET="dhcp"
|
||
fi
|
||
fi
|
||
|
||
IPV6_METHOD=${var_ipv6_method:-"none"}
|
||
IPV6_STATIC=${var_ipv6_static:-""}
|
||
GATE=${var_gateway:-""}
|
||
APT_CACHER=${var_apt_cacher:-""}
|
||
APT_CACHER_IP=${var_apt_cacher_ip:-""}
|
||
|
||
# Runtime check: Verify APT cacher is reachable if configured
|
||
if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then
|
||
local _check_host _check_port _check_url
|
||
_check_host=$(echo "$APT_CACHER_IP" | sed -e 's|https\?://||' -e 's|/.*||' | cut -d: -f1)
|
||
_check_port=$(echo "$APT_CACHER_IP" | sed -e 's|https\?://||' -e 's|/.*||' | cut -s -d: -f2)
|
||
if [[ "$APT_CACHER_IP" =~ ^https?:// ]]; then
|
||
_check_url="$APT_CACHER_IP"
|
||
_check_port="${_check_port:-80}"
|
||
else
|
||
_check_port="${_check_port:-3142}"
|
||
_check_url="http://${APT_CACHER_IP}:${_check_port}"
|
||
fi
|
||
if ! curl -s --connect-timeout 2 "${_check_url}" >/dev/null 2>&1; then
|
||
msg_warn "APT Cacher configured but not reachable at ${_check_url}"
|
||
msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation"
|
||
APT_CACHER=""
|
||
APT_CACHER_IP=""
|
||
else
|
||
msg_ok "APT Cacher verified at ${_check_url}"
|
||
fi
|
||
fi
|
||
|
||
MTU=${var_mtu:-""}
|
||
_sd_val="${var_searchdomain:-""}"
|
||
[[ -n "$_sd_val" ]] && SD="-searchdomain=$_sd_val" || SD=""
|
||
_ns_val="${var_ns:-""}"
|
||
[[ -n "$_ns_val" ]] && NS="-nameserver=$_ns_val" || NS=""
|
||
MAC=${var_mac:-""}
|
||
VLAN=${var_vlan:-""}
|
||
SSH=${var_ssh:-"no"}
|
||
SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
|
||
UDHCPC_FIX=${var_udhcpc_fix:-""}
|
||
TAGS="community-script,${var_tags:-}"
|
||
ENABLE_FUSE=${var_fuse:-"${1:-no}"}
|
||
ENABLE_TUN=${var_tun:-"${1:-no}"}
|
||
|
||
# Additional settings that may be skipped if advanced_settings is not run (e.g., App Defaults)
|
||
ENABLE_GPU=${var_gpu:-"no"}
|
||
ENABLE_NESTING=${var_nesting:-"1"}
|
||
ENABLE_KEYCTL=${var_keyctl:-"0"}
|
||
ENABLE_MKNOD=${var_mknod:-"0"}
|
||
PROTECT_CT=${var_protection:-"no"}
|
||
CT_TIMEZONE=${var_timezone:-"$timezone"}
|
||
[[ "${CT_TIMEZONE:-}" == Etc/* ]] && CT_TIMEZONE="host" # pct doesn't accept Etc/* zones
|
||
|
||
# Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
|
||
if [ -z "$var_os" ]; then
|
||
var_os="debian"
|
||
fi
|
||
if [ -z "$var_version" ]; then
|
||
var_version="12"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# load_vars_file()
|
||
#
|
||
# - Safe parser for KEY=VALUE lines from vars files
|
||
# - Used by default_var_settings and app defaults loading
|
||
# - Only loads whitelisted var_* keys
|
||
# - Optional force parameter to override existing values (for app defaults)
|
||
# ------------------------------------------------------------------------------
|
||
load_vars_file() {
|
||
local file="$1"
|
||
local force="${2:-no}" # If "yes", override existing variables
|
||
[ -f "$file" ] || return 0
|
||
msg_info "Loading defaults from ${file}"
|
||
|
||
# Allowed var_* keys
|
||
local VAR_WHITELIST=(
|
||
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_keyctl
|
||
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
|
||
var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
|
||
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain
|
||
)
|
||
|
||
# Whitelist check helper
|
||
_is_whitelisted() {
|
||
local k="$1" w
|
||
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
|
||
return 1
|
||
}
|
||
|
||
local line key val
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
line="${line#"${line%%[![:space:]]*}"}"
|
||
line="${line%"${line##*[![:space:]]}"}"
|
||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
|
||
local var_key="${BASH_REMATCH[1]}"
|
||
local var_val="${BASH_REMATCH[2]}"
|
||
|
||
[[ "$var_key" != var_* ]] && continue
|
||
_is_whitelisted "$var_key" || continue
|
||
|
||
# Strip inline comments (anything after unquoted #)
|
||
# Only strip if not inside quotes
|
||
if [[ ! "$var_val" =~ ^[\"\'] ]]; then
|
||
var_val="${var_val%%#*}"
|
||
fi
|
||
|
||
# Strip quotes
|
||
if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
|
||
var_val="${BASH_REMATCH[1]}"
|
||
elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
|
||
var_val="${BASH_REMATCH[1]}"
|
||
fi
|
||
|
||
# Trim trailing whitespace
|
||
var_val="${var_val%"${var_val##*[![:space:]]}"}"
|
||
|
||
# Validate values before setting (skip empty values - they use defaults)
|
||
if [[ -n "$var_val" ]]; then
|
||
case "$var_key" in
|
||
var_mac)
|
||
if ! validate_mac_address "$var_val"; then
|
||
msg_warn "Invalid MAC address '$var_val' in $file, ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_vlan)
|
||
if ! validate_vlan_tag "$var_val"; then
|
||
msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_mtu)
|
||
if ! validate_mtu "$var_val"; then
|
||
msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_tags)
|
||
if ! validate_tags "$var_val"; then
|
||
msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_timezone)
|
||
if ! validate_timezone "$var_val"; then
|
||
msg_warn "Invalid timezone '$var_val' in $file, ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_brg)
|
||
if ! validate_bridge "$var_val"; then
|
||
msg_warn "Bridge '$var_val' not found in $file, ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_gateway)
|
||
if ! validate_gateway_ip "$var_val"; then
|
||
msg_warn "Invalid gateway IP '$var_val' in $file, ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_hostname)
|
||
if ! validate_hostname "$var_val"; then
|
||
msg_warn "Invalid hostname '$var_val' in $file, ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_cpu)
|
||
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then
|
||
msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_ram)
|
||
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then
|
||
msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_disk)
|
||
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then
|
||
msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_unprivileged)
|
||
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
|
||
msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_nesting)
|
||
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
|
||
msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring"
|
||
continue
|
||
fi
|
||
# Warn about potential issues with systemd-based OS when nesting is disabled via vars file
|
||
if [[ "$var_val" == "0" && "${var_os:-debian}" != "alpine" ]]; then
|
||
msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation"
|
||
fi
|
||
;;
|
||
var_keyctl)
|
||
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
|
||
msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_net)
|
||
# var_net can be: dhcp, static IP/CIDR, or IP range
|
||
if [[ "$var_val" != "dhcp" ]]; then
|
||
if is_ip_range "$var_val"; then
|
||
: # IP range is valid, will be resolved at runtime
|
||
elif ! validate_ip_address "$var_val"; then
|
||
msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring"
|
||
continue
|
||
fi
|
||
fi
|
||
;;
|
||
var_fuse | var_tun | var_gpu | var_ssh | var_verbose | var_protection)
|
||
if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then
|
||
msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_ipv6_method)
|
||
if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then
|
||
msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_apt_cacher_ip)
|
||
# Allow: plain IP/hostname, http://host, https://host:port
|
||
if [[ -n "$var_val" ]] && ! [[ "$var_val" =~ ^(https?://)?[a-zA-Z0-9._-]+(:[0-9]+)?(/.*)?$ ]]; then
|
||
msg_warn "Invalid APT Cacher address '$var_val' in $file, ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
var_container_storage | var_template_storage)
|
||
# Validate that the storage exists and is active on the current node
|
||
local _storage_status
|
||
_storage_status=$(pvesm status 2>/dev/null | awk -v s="$var_val" '$1 == s { print $3 }')
|
||
if [[ -z "$_storage_status" ]]; then
|
||
msg_warn "Storage '$var_val' from $file not found on this node, ignoring"
|
||
continue
|
||
elif [[ "$_storage_status" == "disabled" ]]; then
|
||
msg_warn "Storage '$var_val' from $file is disabled on this node, ignoring"
|
||
continue
|
||
fi
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
# Set variable: force mode overrides existing, otherwise only set if empty
|
||
if [[ "$force" == "yes" ]]; then
|
||
export "${var_key}=${var_val}"
|
||
else
|
||
[[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
|
||
fi
|
||
fi
|
||
done <"$file"
|
||
msg_ok "Loaded ${file}"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# default_var_settings
|
||
#
|
||
# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
|
||
# - Loads var_* values from default.vars (safe parser, no source/eval)
|
||
# - Precedence: ENV var_* > default.vars > built-in defaults
|
||
# - Maps var_verbose → VERBOSE
|
||
# - Calls base_settings "$VERBOSE" and echo_default
|
||
# ------------------------------------------------------------------------------
|
||
default_var_settings() {
|
||
# Allowed var_* keys (alphabetically sorted)
|
||
# Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
|
||
local VAR_WHITELIST=(
|
||
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_keyctl
|
||
var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
|
||
var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
|
||
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
|
||
)
|
||
|
||
# Snapshot: environment variables (highest precedence)
|
||
declare -A _HARD_ENV=()
|
||
local _k
|
||
for _k in "${VAR_WHITELIST[@]}"; do
|
||
if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
|
||
done
|
||
|
||
# Find default.vars location
|
||
local _find_default_vars
|
||
_find_default_vars() {
|
||
local f
|
||
for f in \
|
||
/usr/local/community-scripts/default.vars \
|
||
"$HOME/.config/community-scripts/default.vars" \
|
||
"./default.vars"; do
|
||
[ -f "$f" ] && {
|
||
echo "$f"
|
||
return 0
|
||
}
|
||
done
|
||
return 252
|
||
}
|
||
# Allow override of storages via env (for non-interactive use cases)
|
||
[ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
|
||
[ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"
|
||
|
||
# Create once, with storages already selected, no var_ctid/var_hostname lines
|
||
local _ensure_default_vars
|
||
_ensure_default_vars() {
|
||
_find_default_vars >/dev/null 2>&1 && return 0
|
||
|
||
local canonical="/usr/local/community-scripts/default.vars"
|
||
# Silent creation - no msg_info output
|
||
mkdir -p /usr/local/community-scripts
|
||
|
||
# Pick storages before writing the file (always ask unless only one)
|
||
# Create a minimal temp file to write into
|
||
: >"$canonical"
|
||
|
||
# Base content (no var_ctid / var_hostname here)
|
||
cat >"$canonical" <<'EOF'
|
||
# Community-Scripts defaults (var_* only). Lines starting with # are comments.
|
||
# Precedence: ENV var_* > default.vars > built-ins.
|
||
# Keep keys alphabetically sorted.
|
||
|
||
# Container type
|
||
var_unprivileged=1
|
||
|
||
# Resources
|
||
var_cpu=1
|
||
var_disk=4
|
||
var_ram=1024
|
||
|
||
# Network
|
||
var_brg=vmbr0
|
||
var_net=dhcp
|
||
var_ipv6_method=none
|
||
# var_gateway=
|
||
# var_vlan=
|
||
# var_mtu=
|
||
# var_mac=
|
||
# var_ns=
|
||
|
||
# SSH
|
||
var_ssh=no
|
||
# var_ssh_authorized_key=
|
||
|
||
# APT cacher (optional - IP or URL)
|
||
# var_apt_cacher=yes
|
||
# var_apt_cacher_ip=192.168.1.10
|
||
# var_apt_cacher_ip=http://proxy.local
|
||
# var_apt_cacher_ip=https://proxy.local:443
|
||
|
||
# Features/Tags/verbosity
|
||
var_fuse=no
|
||
var_tun=no
|
||
|
||
# Advanced Settings (Proxmox-official features)
|
||
var_nesting=1 # Allow nesting (required for Docker/LXC in CT)
|
||
var_keyctl=0 # Allow keyctl() - needed for Docker (systemd-networkd workaround)
|
||
var_mknod=0 # Allow device node creation (requires kernel 5.3+, experimental)
|
||
var_mount_fs= # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults)
|
||
var_protection=no # Prevent accidental deletion of container
|
||
var_timezone= # Container timezone (e.g. Europe/Berlin, leave empty for host timezone)
|
||
var_tags=community-script
|
||
var_verbose=no
|
||
|
||
# Security (root PW) – empty => autologin
|
||
# var_pw=
|
||
|
||
# GitHub Personal Access Token (optional – avoids API rate limits during installs)
|
||
# Create at https://github.com/settings/tokens – read-only public access is sufficient
|
||
# var_github_token=ghp_your_token_here
|
||
EOF
|
||
|
||
# Now choose storages (always prompt unless just one exists)
|
||
choose_and_set_storage_for_file "$canonical" template
|
||
choose_and_set_storage_for_file "$canonical" container
|
||
|
||
chmod 0644 "$canonical"
|
||
# Silent creation - no output message
|
||
}
|
||
|
||
# Whitelist check
|
||
local _is_whitelisted_key
|
||
_is_whitelisted_key() {
|
||
local k="$1"
|
||
local w
|
||
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
|
||
return 1
|
||
}
|
||
|
||
# 1) Ensure file exists
|
||
_ensure_default_vars
|
||
|
||
# 2) Load file
|
||
local dv
|
||
dv="$(_find_default_vars)" || {
|
||
msg_error "default.vars not found after ensure step"
|
||
return 252
|
||
}
|
||
load_vars_file "$dv"
|
||
|
||
# 3) Map var_verbose → VERBOSE
|
||
if [[ -n "${var_verbose:-}" ]]; then
|
||
case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
|
||
else
|
||
VERBOSE="no"
|
||
fi
|
||
|
||
# 4) Map var_github_token → GITHUB_TOKEN (only if not already set in environment)
|
||
if [[ -z "${GITHUB_TOKEN:-}" && -n "${var_github_token:-}" ]]; then
|
||
export GITHUB_TOKEN="${var_github_token}"
|
||
fi
|
||
|
||
# 4) Apply base settings and show summary
|
||
METHOD="mydefaults-global"
|
||
base_settings "$VERBOSE"
|
||
header_info
|
||
echo -e "${DEFAULT}${BOLD}${BL}Using User Defaults (default.vars) on node $PVEHOST_NAME${CL}"
|
||
echo_default
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_app_defaults_path()
|
||
#
|
||
# - Returns full path for app-specific defaults file
|
||
# - Example: /usr/local/community-scripts/defaults/<app>.vars
|
||
# ------------------------------------------------------------------------------
|
||
|
||
get_app_defaults_path() {
|
||
local n="${NSAPP:-${APP,,}}"
|
||
echo "/usr/local/community-scripts/defaults/${n}.vars"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# maybe_offer_save_app_defaults
|
||
#
|
||
# - Called after advanced_settings returned with fully chosen values.
|
||
# - If no <nsapp>.vars exists, offers to persist current advanced settings
|
||
# into /usr/local/community-scripts/defaults/<nsapp>.vars
|
||
# - Only writes whitelisted var_* keys.
|
||
# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
|
||
# ------------------------------------------------------------------------------
|
||
if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
|
||
# Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
|
||
declare -ag VAR_WHITELIST=(
|
||
var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu
|
||
var_gateway var_hostname var_ipv6_method var_mac var_mtu
|
||
var_net var_ns var_os var_pw var_ram var_tags var_tun var_unprivileged
|
||
var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
|
||
)
|
||
fi
|
||
|
||
# Global whitelist check function (used by _load_vars_file_to_map and others)
|
||
_is_whitelisted_key() {
|
||
local k="$1"
|
||
local w
|
||
for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
|
||
return 1
|
||
}
|
||
|
||
_sanitize_value() {
|
||
# Disallow Command-Substitution / Shell-Meta
|
||
case "$1" in
|
||
*'$('* | *'`'* | *';'* | *'&'* | *'<('*)
|
||
echo ""
|
||
return 0
|
||
;;
|
||
esac
|
||
echo "$1"
|
||
}
|
||
|
||
# Map-Parser: read var_* from file into _VARS_IN associative array
|
||
# Note: Main _load_vars_file() with full validation is defined in default_var_settings section
|
||
# This simplified version is used specifically for diff operations via _VARS_IN array
|
||
declare -A _VARS_IN
|
||
_load_vars_file_to_map() {
|
||
local file="$1"
|
||
[ -f "$file" ] || return 0
|
||
_VARS_IN=() # Clear array
|
||
local line key val
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
line="${line#"${line%%[![:space:]]*}"}"
|
||
line="${line%"${line##*[![:space:]]}"}"
|
||
[ -z "$line" ] && continue
|
||
case "$line" in
|
||
\#*) continue ;;
|
||
esac
|
||
key=$(printf "%s" "$line" | cut -d= -f1)
|
||
val=$(printf "%s" "$line" | cut -d= -f2-)
|
||
case "$key" in
|
||
var_*)
|
||
if _is_whitelisted_key "$key"; then
|
||
_VARS_IN["$key"]="$val"
|
||
fi
|
||
;;
|
||
esac
|
||
done <"$file"
|
||
}
|
||
|
||
# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
|
||
_build_vars_diff() {
|
||
local oldf="$1" newf="$2"
|
||
local k
|
||
local -A OLD=() NEW=()
|
||
_load_vars_file_to_map "$oldf"
|
||
for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
|
||
_load_vars_file_to_map "$newf"
|
||
for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done
|
||
|
||
local out
|
||
out+="# Diff for ${APP} (${NSAPP})\n"
|
||
out+="# Old: ${oldf}\n# New: ${newf}\n\n"
|
||
|
||
local found_change=0
|
||
|
||
# Changed & Removed
|
||
for k in "${!OLD[@]}"; do
|
||
if [[ -v NEW["$k"] ]]; then
|
||
if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
|
||
out+="~ ${k}\n - old: ${OLD[$k]}\n + new: ${NEW[$k]}\n"
|
||
found_change=1
|
||
fi
|
||
else
|
||
out+="- ${k}\n - old: ${OLD[$k]}\n"
|
||
found_change=1
|
||
fi
|
||
done
|
||
|
||
# Added
|
||
for k in "${!NEW[@]}"; do
|
||
if [[ ! -v OLD["$k"] ]]; then
|
||
out+="+ ${k}\n + new: ${NEW[$k]}\n"
|
||
found_change=1
|
||
fi
|
||
done
|
||
|
||
if [[ $found_change -eq 0 ]]; then
|
||
out+="(No differences)\n"
|
||
fi
|
||
|
||
printf "%b" "$out"
|
||
}
|
||
|
||
# Build a temporary <app>.vars file from current advanced settings
|
||
_build_current_app_vars_tmp() {
|
||
tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
|
||
|
||
# NET/GW
|
||
_net="${NET:-}"
|
||
_gate=""
|
||
case "${GATE:-}" in
|
||
,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
|
||
esac
|
||
|
||
# IPv6
|
||
_ipv6_method="${IPV6_METHOD:-auto}"
|
||
_ipv6_static=""
|
||
_ipv6_gateway=""
|
||
if [ "$_ipv6_method" = "static" ]; then
|
||
_ipv6_static="${IPV6_ADDR:-}"
|
||
_ipv6_gateway="${IPV6_GATE:-}"
|
||
fi
|
||
|
||
# MTU/VLAN/MAC
|
||
_mtu=""
|
||
_vlan=""
|
||
_mac=""
|
||
case "${MTU:-}" in
|
||
,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
|
||
esac
|
||
case "${VLAN:-}" in
|
||
,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
|
||
esac
|
||
case "${MAC:-}" in
|
||
,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
|
||
esac
|
||
|
||
# DNS / Searchdomain
|
||
_ns=""
|
||
_searchdomain=""
|
||
case "${NS:-}" in
|
||
-nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
|
||
esac
|
||
case "${SD:-}" in
|
||
-searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
|
||
esac
|
||
|
||
# SSH / APT / Features
|
||
_ssh="${SSH:-no}"
|
||
_ssh_auth="${SSH_AUTHORIZED_KEY:-}"
|
||
_apt_cacher="${APT_CACHER:-}"
|
||
_apt_cacher_ip="${APT_CACHER_IP:-}"
|
||
_fuse="${ENABLE_FUSE:-no}"
|
||
_tun="${ENABLE_TUN:-no}"
|
||
_gpu="${ENABLE_GPU:-no}"
|
||
_nesting="${ENABLE_NESTING:-1}"
|
||
_keyctl="${ENABLE_KEYCTL:-0}"
|
||
_mknod="${ENABLE_MKNOD:-0}"
|
||
_mount_fs="${ALLOW_MOUNT_FS:-}"
|
||
_protect="${PROTECT_CT:-no}"
|
||
_timezone="${CT_TIMEZONE:-}"
|
||
_tags="${TAGS:-}"
|
||
_verbose="${VERBOSE:-no}"
|
||
|
||
# Type / Resources / Identity
|
||
_unpriv="${CT_TYPE:-1}"
|
||
_cpu="${CORE_COUNT:-1}"
|
||
_ram="${RAM_SIZE:-1024}"
|
||
_disk="${DISK_SIZE:-4}"
|
||
_hostname="${HN:-$NSAPP}"
|
||
|
||
# Storage
|
||
_tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
|
||
_ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"
|
||
|
||
{
|
||
echo "# App-specific defaults for ${APP} (${NSAPP})"
|
||
echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||
echo
|
||
|
||
echo "var_os=$(_sanitize_value "${var_os:-}")"
|
||
echo "var_version=$(_sanitize_value "${var_version:-}")"
|
||
echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
|
||
echo "var_cpu=$(_sanitize_value "$_cpu")"
|
||
echo "var_ram=$(_sanitize_value "$_ram")"
|
||
echo "var_disk=$(_sanitize_value "$_disk")"
|
||
|
||
[ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
|
||
[ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
|
||
[ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
|
||
[ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
|
||
[ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
|
||
[ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
|
||
[ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"
|
||
|
||
[ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
|
||
# var_ipv6_static removed - static IPs are unique, can't be default
|
||
|
||
[ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
|
||
[ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"
|
||
|
||
[ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
|
||
[ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"
|
||
|
||
[ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
|
||
[ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
|
||
[ -n "$_gpu" ] && echo "var_gpu=$(_sanitize_value "$_gpu")"
|
||
[ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")"
|
||
[ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")"
|
||
[ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")"
|
||
[ -n "$_mount_fs" ] && echo "var_mount_fs=$(_sanitize_value "$_mount_fs")"
|
||
[ -n "$_protect" ] && echo "var_protection=$(_sanitize_value "$_protect")"
|
||
[ -n "$_timezone" ] && echo "var_timezone=$(_sanitize_value "$_timezone")"
|
||
[ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
|
||
[ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"
|
||
|
||
[ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
|
||
[ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"
|
||
|
||
[ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
|
||
[ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
|
||
} >"$tmpf"
|
||
|
||
echo "$tmpf"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# maybe_offer_save_app_defaults()
|
||
#
|
||
# - Called after advanced_settings()
|
||
# - Offers to save current values as app defaults if not existing
|
||
# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
|
||
# ------------------------------------------------------------------------------
|
||
maybe_offer_save_app_defaults() {
|
||
local app_vars_path
|
||
app_vars_path="$(get_app_defaults_path)"
|
||
|
||
# always build from current settings
|
||
local new_tmp diff_tmp
|
||
new_tmp="$(_build_current_app_vars_tmp)"
|
||
diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"
|
||
|
||
# 1) if no file → offer to create
|
||
if [[ ! -f "$app_vars_path" ]]; then
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then
|
||
mkdir -p "$(dirname "$app_vars_path")"
|
||
/usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
|
||
msg_ok "Saved app defaults: ${app_vars_path}"
|
||
fi
|
||
rm -f "$new_tmp" "$diff_tmp"
|
||
return 0
|
||
fi
|
||
|
||
# 2) if file exists → build diff
|
||
_build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"
|
||
|
||
# if no differences → do nothing
|
||
if grep -q "^(No differences)$" "$diff_tmp"; then
|
||
rm -f "$new_tmp" "$diff_tmp"
|
||
return 0
|
||
fi
|
||
|
||
# 3) if file exists → show menu with default selection "Update Defaults"
|
||
local app_vars_file
|
||
app_vars_file="$(basename "$app_vars_path")"
|
||
|
||
while true; do
|
||
local sel
|
||
sel="$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "APP DEFAULTS – ${APP}" \
|
||
--menu "Differences detected. What do you want to do?" 20 78 10 \
|
||
"Update Defaults" "Write new values to ${app_vars_file}" \
|
||
"Keep Current" "Keep existing defaults (no changes)" \
|
||
"View Diff" "Show a detailed diff" \
|
||
"Cancel" "Abort without changes" \
|
||
--default-item "Update Defaults" \
|
||
3>&1 1>&2 2>&3)" || { sel="Cancel"; }
|
||
|
||
case "$sel" in
|
||
"Update Defaults")
|
||
/usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
|
||
msg_ok "Updated app defaults: ${app_vars_path}"
|
||
break
|
||
;;
|
||
"Keep Current")
|
||
msg_custom "ℹ️" "${BL}" "Keeping current app defaults: ${app_vars_path}"
|
||
break
|
||
;;
|
||
"View Diff")
|
||
whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "Diff – ${APP}" \
|
||
--scrolltext --textbox "$diff_tmp" 25 100
|
||
;;
|
||
"Cancel" | *)
|
||
msg_custom "🚫" "${YW}" "Canceled. No changes to app defaults."
|
||
break
|
||
;;
|
||
esac
|
||
done
|
||
|
||
rm -f "$new_tmp" "$diff_tmp"
|
||
}
|
||
|
||
ensure_storage_selection_for_vars_file() {
|
||
local vf="$1"
|
||
|
||
# Read stored values (if any)
|
||
local tpl ct
|
||
tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2- || true)
|
||
ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2- || true)
|
||
|
||
if [[ -n "$tpl" && -n "$ct" ]]; then
|
||
TEMPLATE_STORAGE="$tpl"
|
||
CONTAINER_STORAGE="$ct"
|
||
|
||
# Validate storage space for loaded container storage
|
||
if [[ -n "${DISK_SIZE:-}" ]]; then
|
||
validate_storage_space "$ct" "$DISK_SIZE" "yes"
|
||
# Continue even if validation fails - user was warned
|
||
fi
|
||
|
||
return 0
|
||
fi
|
||
|
||
choose_and_set_storage_for_file "$vf" template
|
||
choose_and_set_storage_for_file "$vf" container
|
||
|
||
# Silent operation - no output message
|
||
}
|
||
|
||
ensure_global_default_vars_file() {
|
||
local vars_path="/usr/local/community-scripts/default.vars"
|
||
if [[ ! -f "$vars_path" ]]; then
|
||
mkdir -p "$(dirname "$vars_path")"
|
||
touch "$vars_path"
|
||
fi
|
||
echo "$vars_path"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# advanced_settings()
|
||
#
|
||
# - Interactive wizard-style configuration with BACK navigation
|
||
# - State-machine approach: each step can go forward or backward
|
||
# - Cancel at Step 1 = Exit Script, Cancel at other steps = Go Back
|
||
# - Allows user to customize all container settings
|
||
# ------------------------------------------------------------------------------
|
||
advanced_settings() {
|
||
# Enter alternate screen buffer to prevent flicker between dialogs
|
||
tput smcup 2>/dev/null || true
|
||
trap 'tput rmcup 2>/dev/null || true' RETURN
|
||
|
||
# Initialize defaults
|
||
TAGS="community-script;${var_tags:-}"
|
||
local STEP=1
|
||
local MAX_STEP=28
|
||
|
||
# Store values for back navigation - inherit from var_* app defaults
|
||
local _ct_type="${var_unprivileged:-1}"
|
||
local _pw=""
|
||
local _pw_display="Automatic Login"
|
||
local _ct_id="$NEXTID"
|
||
local _hostname="$NSAPP"
|
||
local _disk_size="${var_disk:-4}"
|
||
local _core_count="${var_cpu:-1}"
|
||
local _ram_size="${var_ram:-1024}"
|
||
local _bridge="${var_brg:-vmbr0}"
|
||
local _net="${var_net:-dhcp}"
|
||
local _gate="${var_gateway:-}"
|
||
local _ipv6_method="${var_ipv6_method:-auto}"
|
||
local _ipv6_addr=""
|
||
local _ipv6_gate=""
|
||
local _apt_cacher="${var_apt_cacher:-no}"
|
||
local _apt_cacher_ip="${var_apt_cacher_ip:-}"
|
||
local _mtu="${var_mtu:-}"
|
||
local _sd="${var_searchdomain:-}"
|
||
local _ns="${var_ns:-}"
|
||
local _mac="${var_mac:-}"
|
||
local _vlan="${var_vlan:-}"
|
||
local _tags="$TAGS"
|
||
local _enable_fuse="${var_fuse:-no}"
|
||
local _enable_tun="${var_tun:-no}"
|
||
local _enable_gpu="${var_gpu:-no}"
|
||
local _enable_nesting="${var_nesting:-1}"
|
||
local _verbose="${var_verbose:-no}"
|
||
local _enable_keyctl="${var_keyctl:-0}"
|
||
local _enable_mknod="${var_mknod:-0}"
|
||
local _mount_fs="${var_mount_fs:-}"
|
||
local _protect_ct="${var_protection:-no}"
|
||
|
||
# Detect host timezone for default (if not set via var_timezone)
|
||
local _host_timezone=""
|
||
if command -v timedatectl >/dev/null 2>&1; then
|
||
_host_timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "")
|
||
elif [ -f /etc/timezone ]; then
|
||
_host_timezone=$(cat /etc/timezone 2>/dev/null || echo "")
|
||
fi
|
||
# Map Etc/* timezones to "host" (pct doesn't accept Etc/* zones)
|
||
[[ "${_host_timezone:-}" == Etc/* ]] && _host_timezone="host"
|
||
local _ct_timezone="${var_timezone:-$_host_timezone}"
|
||
[[ "${_ct_timezone:-}" == Etc/* ]] && _ct_timezone="host"
|
||
|
||
# Helper to show current progress
|
||
show_progress() {
|
||
local current=$1
|
||
local total=$MAX_STEP
|
||
echo -e "\n${INFO}${BOLD}${DGN}Step $current of $total${CL}"
|
||
}
|
||
|
||
# Detect available bridges (do this once)
|
||
local BRIDGES=""
|
||
local BRIDGE_MENU_OPTIONS=()
|
||
_detect_bridges() {
|
||
IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f 2>/dev/null)
|
||
BRIDGES=""
|
||
local OLD_IFS=$IFS
|
||
IFS=$'\n'
|
||
for iface_filepath in ${IFACE_FILEPATH_LIST}; do
|
||
local iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
|
||
(grep -Pn '^\s*iface' "${iface_filepath}" 2>/dev/null | cut -d':' -f1 && wc -l "${iface_filepath}" 2>/dev/null | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" 2>/dev/null || true
|
||
if [ -f "${iface_indexes_tmpfile}" ]; then
|
||
while read -r pair; do
|
||
local start=$(echo "${pair}" | cut -d':' -f1)
|
||
local end=$(echo "${pair}" | cut -d':' -f2)
|
||
if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" 2>/dev/null | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
|
||
local iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
|
||
BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
|
||
fi
|
||
done <"${iface_indexes_tmpfile}"
|
||
rm -f "${iface_indexes_tmpfile}"
|
||
fi
|
||
done
|
||
IFS=$OLD_IFS
|
||
BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
|
||
|
||
# Build bridge menu
|
||
BRIDGE_MENU_OPTIONS=()
|
||
if [[ -n "$BRIDGES" ]]; then
|
||
while IFS= read -r bridge; do
|
||
if [[ -n "$bridge" ]]; then
|
||
local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//;s/^[- ]*//' || true)
|
||
BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }")
|
||
fi
|
||
done <<<"$BRIDGES"
|
||
fi
|
||
}
|
||
_detect_bridges
|
||
|
||
# Main wizard loop
|
||
while [ $STEP -le $MAX_STEP ]; do
|
||
case $STEP in
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 1: Container Type
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
1)
|
||
local default_on="ON"
|
||
local default_off="OFF"
|
||
[[ "$_ct_type" == "0" ]] && {
|
||
default_on="OFF"
|
||
default_off="ON"
|
||
}
|
||
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONTAINER TYPE" \
|
||
--ok-button "Next" --cancel-button "Exit" \
|
||
--radiolist "\nChoose container type:\n\nUse SPACE to select, ENTER to confirm." 14 58 2 \
|
||
"1" "Unprivileged (recommended)" $default_on \
|
||
"0" "Privileged" $default_off \
|
||
3>&1 1>&2 2>&3); then
|
||
[[ -n "$result" ]] && _ct_type="$result"
|
||
((STEP++))
|
||
else
|
||
exit_script
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 2: Root Password
|
||
# ════════════════════════════════════════<E29590><E29590><EFBFBD>═══════════════════════════════<E29590><E29590><EFBFBD>══
|
||
2)
|
||
if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "ROOT PASSWORD" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--passwordbox "\nSet Root Password (needed for root ssh access)\n\nLeave blank for automatic login (no password)" 12 58 \
|
||
3>&1 1>&2 2>&3); then
|
||
|
||
if [[ -z "$PW1" ]]; then
|
||
_pw=""
|
||
_pw_display="Automatic Login"
|
||
((STEP++))
|
||
elif [[ "$PW1" == *" "* ]]; then
|
||
whiptail --msgbox "Password cannot contain spaces." 8 58
|
||
else
|
||
local _pw1_clean="$PW1"
|
||
while [[ "$_pw1_clean" == -* ]]; do
|
||
_pw1_clean="${_pw1_clean#-}"
|
||
done
|
||
if [[ -z "$_pw1_clean" ]]; then
|
||
whiptail --msgbox "Password cannot be only '-' characters." 8 58
|
||
continue
|
||
elif ((${#_pw1_clean} < 5)); then
|
||
whiptail --msgbox "Password must be at least 5 characters (after removing leading '-')." 8 70
|
||
continue
|
||
fi
|
||
# Verify password
|
||
if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "PASSWORD VERIFICATION" \
|
||
--ok-button "Confirm" --cancel-button "Back" \
|
||
--passwordbox "\nVerify Root Password" 10 58 \
|
||
3>&1 1>&2 2>&3); then
|
||
local _pw2_clean="$PW2"
|
||
while [[ "$_pw2_clean" == -* ]]; do
|
||
_pw2_clean="${_pw2_clean#-}"
|
||
done
|
||
if [[ "$_pw1_clean" == "$_pw2_clean" ]]; then
|
||
_pw="--password $_pw1_clean"
|
||
_pw_display="********"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Passwords do not match. Please try again." 8 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 3: Container ID
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
3)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONTAINER ID" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Container ID" 10 58 "$_ct_id" \
|
||
3>&1 1>&2 2>&3); then
|
||
local input_id="${result:-$NEXTID}"
|
||
|
||
# Validate that ID is numeric
|
||
if ! [[ "$input_id" =~ ^[0-9]+$ ]]; then
|
||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "Invalid ID" --msgbox "Container ID must be numeric." 8 58
|
||
continue
|
||
fi
|
||
|
||
# Check if ID is already in use
|
||
if ! validate_container_id "$input_id"; then
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" \
|
||
--yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID ($(get_valid_container_id "$input_id"))?" 10 58; then
|
||
_ct_id=$(get_valid_container_id "$input_id")
|
||
else
|
||
continue
|
||
fi
|
||
else
|
||
_ct_id="$input_id"
|
||
fi
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 4: Hostname
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
4)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "HOSTNAME" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Hostname (or FQDN, e.g. host.example.com)" 10 58 "$_hostname" \
|
||
3>&1 1>&2 2>&3); then
|
||
local hn_test="${result:-$NSAPP}"
|
||
hn_test=$(echo "${hn_test,,}" | tr -d ' ')
|
||
|
||
if validate_hostname "$hn_test"; then
|
||
_hostname="$hn_test"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid hostname: '$hn_test'\n\nRules:\n- Only lowercase letters, digits, dots and hyphens\n- Labels separated by dots (max 63 chars each)\n- No leading/trailing hyphens or dots\n- No consecutive dots\n- Total max 253 characters" 14 60
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 5: Disk Size
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
5)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "DISK SIZE" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Disk Size in GB" 10 58 "$_disk_size" \
|
||
3>&1 1>&2 2>&3); then
|
||
local disk_test="${result:-$var_disk}"
|
||
if [[ "$disk_test" =~ ^[1-9][0-9]*$ ]]; then
|
||
_disk_size="$disk_test"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Disk size must be a positive integer!" 8 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 6: CPU Cores
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
6)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CPU CORES" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nAllocate CPU Cores" 10 58 "$_core_count" \
|
||
3>&1 1>&2 2>&3); then
|
||
local cpu_test="${result:-$var_cpu}"
|
||
if [[ "$cpu_test" =~ ^[1-9][0-9]*$ ]]; then
|
||
_core_count="$cpu_test"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "CPU core count must be a positive integer!" 8 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 7: RAM Size
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
7)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "RAM SIZE" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nAllocate RAM in MiB" 10 58 "$_ram_size" \
|
||
3>&1 1>&2 2>&3); then
|
||
local ram_test="${result:-$var_ram}"
|
||
if [[ "$ram_test" =~ ^[1-9][0-9]*$ ]]; then
|
||
_ram_size="$ram_test"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "RAM size must be a positive integer!" 8 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 8: Network Bridge
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
8)
|
||
if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then
|
||
# Validate default bridge exists
|
||
if validate_bridge "vmbr0"; then
|
||
_bridge="vmbr0"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Default bridge 'vmbr0' not found!\n\nPlease configure a network bridge in Proxmox first." 10 58
|
||
msg_error "Default bridge 'vmbr0' not found"
|
||
exit 116
|
||
fi
|
||
else
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "NETWORK BRIDGE" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--menu "\nSelect network bridge:" 16 58 6 \
|
||
"${BRIDGE_MENU_OPTIONS[@]}" \
|
||
3>&1 1>&2 2>&3); then
|
||
local bridge_test="${result:-vmbr0}"
|
||
# Skip separator entries (e.g., __other__) - re-display menu
|
||
if [[ "$bridge_test" == "__other__" || "$bridge_test" == -* ]]; then
|
||
continue
|
||
fi
|
||
if validate_bridge "$bridge_test"; then
|
||
_bridge="$bridge_test"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Bridge '$bridge_test' is not available or not active." 8 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 9: IPv4 Configuration
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
9)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "IPv4 CONFIGURATION" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--menu "\nSelect IPv4 Address Assignment:" 16 65 3 \
|
||
"dhcp" "Automatic (DHCP, recommended)" \
|
||
"static" "Static (manual entry)" \
|
||
"range" "IP Range Scan (find first free IP)" \
|
||
3>&1 1>&2 2>&3); then
|
||
|
||
if [[ "$result" == "static" ]]; then
|
||
# Get static IP
|
||
local static_ip
|
||
if static_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "STATIC IPv4 ADDRESS" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if validate_ip_address "$static_ip"; then
|
||
# Get gateway
|
||
local gateway_ip
|
||
if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "GATEWAY IP" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nEnter Gateway IP address" 10 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if validate_gateway_ip "$gateway_ip"; then
|
||
# Validate gateway is in same subnet
|
||
if validate_gateway_in_subnet "$static_ip" "$gateway_ip"; then
|
||
_net="$static_ip"
|
||
_gate=",gw=$gateway_ip"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Gateway is not in the same subnet as the static IP.\n\nStatic IP: $static_ip\nGateway: $gateway_ip" 10 58
|
||
fi
|
||
else
|
||
whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
|
||
fi
|
||
fi
|
||
else
|
||
whiptail --msgbox "Invalid IPv4 CIDR format.\n\nEach octet must be 0-255.\nCIDR must be 1-32.\nExample: 192.168.1.100/24" 12 58
|
||
fi
|
||
fi
|
||
elif [[ "$result" == "range" ]]; then
|
||
# IP Range Scan
|
||
local ip_range
|
||
if ip_range=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "IP RANGE SCAN" \
|
||
--ok-button "Scan" --cancel-button "Back" \
|
||
--inputbox "\nEnter IP range to scan for free address\n(e.g. 192.168.1.100/24-192.168.1.200/24)" 12 65 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if is_ip_range "$ip_range"; then
|
||
# Exit whiptail screen temporarily to show scan progress
|
||
clear
|
||
header_info
|
||
echo -e "${INFO}${BOLD}${DGN}Scanning IP range for free address...${CL}\n"
|
||
if resolve_ip_from_range "$ip_range"; then
|
||
# Get gateway
|
||
local gateway_ip
|
||
if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "GATEWAY IP" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nFound free IP: $NET_RESOLVED\n\nEnter Gateway IP address" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if validate_gateway_ip "$gateway_ip"; then
|
||
# Validate gateway is in same subnet
|
||
if validate_gateway_in_subnet "$NET_RESOLVED" "$gateway_ip"; then
|
||
_net="$NET_RESOLVED"
|
||
_gate=",gw=$gateway_ip"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Gateway is not in the same subnet as the IP.\n\nIP: $NET_RESOLVED\nGateway: $gateway_ip" 10 58
|
||
fi
|
||
else
|
||
whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
|
||
fi
|
||
fi
|
||
else
|
||
whiptail --msgbox "No free IP found in the specified range.\nAll IPs responded to ping." 10 58
|
||
fi
|
||
else
|
||
whiptail --msgbox "Invalid IP range format.\n\nExample: 192.168.1.100/24-192.168.1.200/24" 10 58
|
||
fi
|
||
fi
|
||
else
|
||
_net="dhcp"
|
||
_gate=""
|
||
((STEP++))
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 10: IPv6 Configuration
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
10)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "IPv6 CONFIGURATION" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--menu "\nSelect IPv6 Address Management:" 16 70 5 \
|
||
"auto" "SLAAC/AUTO (recommended) - Dynamic IPv6 from network" \
|
||
"dhcp" "DHCPv6 - DHCP-assigned IPv6 address" \
|
||
"static" "Static - Manual IPv6 address configuration" \
|
||
"none" "None - No IPv6 assignment (most containers)" \
|
||
"disable" "Fully Disabled - (breaks some services)" \
|
||
3>&1 1>&2 2>&3); then
|
||
|
||
_ipv6_method="$result"
|
||
case "$result" in
|
||
static)
|
||
local ipv6_addr
|
||
if ipv6_addr=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "STATIC IPv6 ADDRESS" \
|
||
--inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if validate_ipv6_address "$ipv6_addr"; then
|
||
_ipv6_addr="$ipv6_addr"
|
||
# Optional gateway - loop until valid or empty
|
||
local ipv6_gw_valid=false
|
||
while [[ "$ipv6_gw_valid" == "false" ]]; do
|
||
local ipv6_gw
|
||
ipv6_gw=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "IPv6 GATEWAY" \
|
||
--inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \
|
||
3>&1 1>&2 2>&3) || true
|
||
# Validate gateway if provided
|
||
if [[ -n "$ipv6_gw" ]]; then
|
||
if validate_ipv6_address "$ipv6_gw"; then
|
||
_ipv6_gate="$ipv6_gw"
|
||
ipv6_gw_valid=true
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid IPv6 gateway format.\n\nExample: 2001:db8::1" 8 58
|
||
fi
|
||
else
|
||
_ipv6_gate=""
|
||
ipv6_gw_valid=true
|
||
((STEP++))
|
||
fi
|
||
done
|
||
else
|
||
whiptail --msgbox "Invalid IPv6 CIDR format.\n\nExample: 2001:db8::1/64\nCIDR must be 1-128." 10 58
|
||
fi
|
||
fi
|
||
;;
|
||
dhcp)
|
||
_ipv6_addr="dhcp"
|
||
_ipv6_gate=""
|
||
((STEP++))
|
||
;;
|
||
|
||
none)
|
||
_ipv6_addr="none"
|
||
_ipv6_gate=""
|
||
((STEP++))
|
||
;;
|
||
*)
|
||
_ipv6_addr=""
|
||
_ipv6_gate=""
|
||
((STEP++))
|
||
;;
|
||
esac
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 11: MTU Size
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
11)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "MTU SIZE" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Interface MTU Size\n(leave blank for default 1500, common values: 1500, 9000)" 12 62 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if validate_mtu "$result"; then
|
||
_mtu="$result"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid MTU size.\n\nMTU must be between 576 and 65535.\nCommon values: 1500 (default), 9000 (jumbo frames)" 10 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 12: DNS Search Domain
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
12)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "DNS SEARCH DOMAIN" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet DNS Search Domain\n(leave blank to use host setting)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
_sd="$result"
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 13: DNS Server
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
13)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "DNS SERVER" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet DNS Server IP\n(leave blank to use host setting)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
_ns="$result"
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 14: MAC Address
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
14)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "MAC ADDRESS" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet MAC Address\n(leave blank for auto-generated, format: XX:XX:XX:XX:XX:XX)" 12 62 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if validate_mac_address "$result"; then
|
||
_mac="$result"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid MAC address format.\n\nRequired format: XX:XX:XX:XX:XX:XX\nExample: 02:00:00:00:00:01" 10 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 15: VLAN Tag
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
15)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "VLAN TAG" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet VLAN Tag (1-4094)\n(leave blank for no VLAN)" 12 58 "" \
|
||
3>&1 1>&2 2>&3); then
|
||
if validate_vlan_tag "$result"; then
|
||
_vlan="$result"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid VLAN tag.\n\nVLAN must be a number between 1 and 4094." 8 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 16: Tags
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
16)
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONTAINER TAGS" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet Custom Tags (semicolon-separated)\n(alphanumeric, hyphens, underscores only)" 12 58 "$_tags" \
|
||
3>&1 1>&2 2>&3); then
|
||
local tags_test="${result:-}"
|
||
tags_test=$(echo "$tags_test" | tr -d '[:space:]')
|
||
if validate_tags "$tags_test"; then
|
||
_tags="$tags_test"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid tag format.\n\nTags can only contain:\n- Letters (a-z, A-Z)\n- Numbers (0-9)\n- Hyphens (-)\n- Underscores (_)\n- Semicolons (;) as separator" 14 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 17: SSH Settings
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
17)
|
||
configure_ssh_settings "Step $STEP/$MAX_STEP"
|
||
# configure_ssh_settings handles its own flow, always advance
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 18: FUSE Support
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
18)
|
||
local fuse_default_flag="--defaultno"
|
||
[[ "$_enable_fuse" == "yes" || "$_enable_fuse" == "1" ]] && fuse_default_flag=""
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "FUSE SUPPORT" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
$fuse_default_flag \
|
||
--yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc.\n\n(App default: ${var_fuse:-no})" 14 58; then
|
||
_enable_fuse="yes"
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_enable_fuse="no"
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 19: TUN/TAP Support
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
19)
|
||
local tun_default_flag="--defaultno"
|
||
[[ "$_enable_tun" == "yes" || "$_enable_tun" == "1" ]] && tun_default_flag=""
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "TUN/TAP SUPPORT" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
$tun_default_flag \
|
||
--yesno "\nEnable TUN/TAP device support?\n\nRequired for: VPN apps (WireGuard, OpenVPN, Tailscale),\nnetwork tunneling, and containerized networking.\n\n(App default: ${var_tun:-no})" 14 62; then
|
||
_enable_tun="yes"
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_enable_tun="no"
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 20: Nesting Support
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
20)
|
||
local nesting_default_flag=""
|
||
[[ "$_enable_nesting" == "0" || "$_enable_nesting" == "no" ]] && nesting_default_flag="--defaultno"
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "NESTING SUPPORT" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
$nesting_default_flag \
|
||
--yesno "\nEnable Nesting?\n\nRequired for: Docker, LXC inside LXC, Podman,\nand other containerization tools.\n\n(App default: ${var_nesting:-1})" 14 58; then
|
||
_enable_nesting="1"
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_enable_nesting="0"
|
||
# Warn about potential issues with systemd-based OS when nesting is disabled
|
||
if [[ "$var_os" != "alpine" ]]; then
|
||
whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "⚠️ NESTING WARNING" \
|
||
--msgbox "Modern systemd-based distributions (Debian 13+, Ubuntu 24.04+, etc.) may require nesting to be enabled for proper operation.\n\nWithout nesting, the container may start in a degraded state with failing services (error 243/CREDENTIALS).\n\nIf you experience issues, enable nesting in the container options." 14 68
|
||
fi
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 21: GPU Passthrough
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
21)
|
||
local gpu_default_flag="--defaultno"
|
||
[[ "$_enable_gpu" == "yes" ]] && gpu_default_flag=""
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "GPU PASSTHROUGH" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
$gpu_default_flag \
|
||
--yesno "\nEnable GPU Passthrough?\n\nAutomatically detects and passes through available GPUs\n(Intel/AMD/NVIDIA) for hardware acceleration.\n\nRecommended for: Media servers, AI/ML, Transcoding\n\n(App default: ${var_gpu:-no})" 16 62; then
|
||
_enable_gpu="yes"
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_enable_gpu="no"
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 22: Keyctl Support (Docker/systemd)
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
22)
|
||
local keyctl_default_flag="--defaultno"
|
||
[[ "$_enable_keyctl" == "1" ]] && keyctl_default_flag=""
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "KEYCTL SUPPORT" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
$keyctl_default_flag \
|
||
--yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\nNote: Automatically enabled for unprivileged containers.\n\n(App default: ${var_keyctl:-0})" 16 62; then
|
||
_enable_keyctl="1"
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_enable_keyctl="0"
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 23: APT Cacher Proxy
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
23)
|
||
local apt_cacher_default_flag="--defaultno"
|
||
[[ "$_apt_cacher" == "yes" ]] && apt_cacher_default_flag=""
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "APT CACHER PROXY" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
$apt_cacher_default_flag \
|
||
--yesno "\nUse APT Cacher-NG proxy?\n\nSpeeds up package downloads by caching them locally.\nRequires apt-cacher-ng running on your network.\n\n(App default: ${var_apt_cacher:-no})" 14 62; then
|
||
_apt_cacher="yes"
|
||
# Ask for IP if enabled
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "APT CACHER IP" \
|
||
--inputbox "\nEnter APT Cacher-NG IP or URL:\n(e.g. 192.168.1.10, http://host, https://host:443)" 12 62 "$_apt_cacher_ip" \
|
||
3>&1 1>&2 2>&3); then
|
||
_apt_cacher_ip="$result"
|
||
fi
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_apt_cacher="no"
|
||
_apt_cacher_ip=""
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 24: Container Timezone
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
24)
|
||
local tz_hint="$_ct_timezone"
|
||
[[ -z "$tz_hint" ]] && tz_hint="(empty - will use host timezone)"
|
||
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONTAINER TIMEZONE" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${_host_timezone:-unknown}\n\nLeave empty to inherit from host." 16 62 "$_ct_timezone" \
|
||
3>&1 1>&2 2>&3); then
|
||
local tz_test="$result"
|
||
[[ "${tz_test:-}" == Etc/* ]] && tz_test="host" # pct doesn't accept Etc/* zones
|
||
if validate_timezone "$tz_test"; then
|
||
_ct_timezone="$tz_test"
|
||
((STEP++))
|
||
else
|
||
whiptail --msgbox "Invalid timezone: '$result'\n\nTimezone must exist in /usr/share/zoneinfo/\n\nExamples:\n- Europe/Berlin\n- America/New_York\n- Asia/Tokyo\n- UTC" 14 58
|
||
fi
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 25: Container Protection
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
25)
|
||
local protect_default_flag="--defaultno"
|
||
[[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_default_flag=""
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONTAINER PROTECTION" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
$protect_default_flag \
|
||
--yesno "\nEnable Container Protection?\n\nPrevents accidental deletion of this container.\nYou must disable protection before removing.\n\n(App default: ${var_protection:-no})" 14 62; then
|
||
_protect_ct="yes"
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_protect_ct="no"
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 26: Device Node Creation (mknod)
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
26)
|
||
local mknod_default_flag="--defaultno"
|
||
[[ "$_enable_mknod" == "1" ]] && mknod_default_flag=""
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "DEVICE NODE CREATION" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
$mknod_default_flag \
|
||
--yesno "\nAllow device node creation (mknod)?\n\nRequired for: Creating device files inside container.\nExperimental feature (requires kernel 5.3+).\n\n(App default: ${var_mknod:-0})" 14 62; then
|
||
_enable_mknod="1"
|
||
else
|
||
if [ $? -eq 1 ]; then
|
||
_enable_mknod="0"
|
||
else
|
||
((STEP--))
|
||
continue
|
||
fi
|
||
fi
|
||
((STEP++))
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 27: Mount Filesystems
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
27)
|
||
local mount_hint=""
|
||
[[ -n "$_mount_fs" ]] && mount_hint="$_mount_fs" || mount_hint="(none)"
|
||
|
||
if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "MOUNT FILESYSTEMS" \
|
||
--ok-button "Next" --cancel-button "Back" \
|
||
--inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint" 14 62 "$_mount_fs" \
|
||
3>&1 1>&2 2>&3); then
|
||
_mount_fs="$result"
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# STEP 28: Verbose Mode & Confirmation
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
28)
|
||
local verbose_default_flag="--defaultno"
|
||
[[ "$_verbose" == "yes" ]] && verbose_default_flag=""
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "VERBOSE MODE" \
|
||
$verbose_default_flag \
|
||
--yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then
|
||
_verbose="yes"
|
||
else
|
||
_verbose="no"
|
||
fi
|
||
# Build summary
|
||
local ct_type_desc="Unprivileged"
|
||
[[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged"
|
||
|
||
local nesting_desc="Disabled"
|
||
[[ "$_enable_nesting" == "1" ]] && nesting_desc="Enabled"
|
||
|
||
local keyctl_desc="Disabled"
|
||
[[ "$_enable_keyctl" == "1" ]] && keyctl_desc="Enabled"
|
||
|
||
local protect_desc="No"
|
||
[[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_desc="Yes"
|
||
|
||
local tz_display="${_ct_timezone:-Host TZ}"
|
||
local apt_display="${_apt_cacher:-no}"
|
||
[[ "$_apt_cacher" == "yes" && -n "$_apt_cacher_ip" ]] && apt_display="$_apt_cacher_ip"
|
||
|
||
local summary="Container Type: $ct_type_desc
|
||
Container ID: $_ct_id
|
||
Hostname: $_hostname
|
||
|
||
Resources:
|
||
Disk: ${_disk_size} GB
|
||
CPU: $_core_count cores
|
||
RAM: $_ram_size MiB
|
||
|
||
Network:
|
||
Bridge: $_bridge
|
||
IPv4: $_net
|
||
IPv6: $_ipv6_method
|
||
|
||
Features:
|
||
FUSE: $_enable_fuse | TUN: $_enable_tun
|
||
Nesting: $nesting_desc | Keyctl: $keyctl_desc
|
||
GPU: $_enable_gpu | Protection: $protect_desc
|
||
|
||
Advanced:
|
||
Timezone: $tz_display
|
||
APT Cacher: $apt_display
|
||
Verbose: $_verbose"
|
||
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
|
||
--title "CONFIRM SETTINGS" \
|
||
--ok-button "Create LXC" --cancel-button "Back" \
|
||
--yesno "$summary\n\nCreate ${APP} LXC with these settings?" 32 62; then
|
||
((STEP++))
|
||
else
|
||
((STEP--))
|
||
fi
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# Apply all collected values to global variables
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
CT_TYPE="$_ct_type"
|
||
PW="$_pw"
|
||
CT_ID="$_ct_id"
|
||
HN="$_hostname"
|
||
DISK_SIZE="$_disk_size"
|
||
CORE_COUNT="$_core_count"
|
||
RAM_SIZE="$_ram_size"
|
||
BRG="$_bridge"
|
||
NET="$_net"
|
||
GATE="$_gate"
|
||
IPV6_METHOD="$_ipv6_method"
|
||
IPV6_ADDR="$_ipv6_addr"
|
||
IPV6_GATE="$_ipv6_gate"
|
||
TAGS="$_tags"
|
||
ENABLE_FUSE="$_enable_fuse"
|
||
ENABLE_TUN="$_enable_tun"
|
||
ENABLE_GPU="$_enable_gpu"
|
||
ENABLE_NESTING="$_enable_nesting"
|
||
ENABLE_KEYCTL="$_enable_keyctl"
|
||
ENABLE_MKNOD="$_enable_mknod"
|
||
ALLOW_MOUNT_FS="$_mount_fs"
|
||
PROTECT_CT="$_protect_ct"
|
||
CT_TIMEZONE="$_ct_timezone"
|
||
APT_CACHER="$_apt_cacher"
|
||
APT_CACHER_IP="$_apt_cacher_ip"
|
||
VERBOSE="$_verbose"
|
||
|
||
# Update var_* based on user choice (for functions that check these)
|
||
var_gpu="$_enable_gpu"
|
||
var_fuse="$_enable_fuse"
|
||
var_tun="$_enable_tun"
|
||
var_nesting="$_enable_nesting"
|
||
var_keyctl="$_enable_keyctl"
|
||
var_mknod="$_enable_mknod"
|
||
var_mount_fs="$_mount_fs"
|
||
var_protection="$_protect_ct"
|
||
var_timezone="$_ct_timezone"
|
||
var_apt_cacher="$_apt_cacher"
|
||
var_apt_cacher_ip="$_apt_cacher_ip"
|
||
|
||
# Format optional values
|
||
[[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU=""
|
||
[[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD=""
|
||
[[ -n "$_ns" ]] && NS="-nameserver=$_ns" || NS=""
|
||
[[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC=""
|
||
[[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN=""
|
||
|
||
# Alpine UDHCPC fix
|
||
if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ -n "$_ns" ]; then
|
||
UDHCPC_FIX="yes"
|
||
else
|
||
UDHCPC_FIX="no"
|
||
fi
|
||
export UDHCPC_FIX
|
||
export SSH_KEYS_FILE
|
||
|
||
# Exit alternate screen buffer before showing summary (so output remains visible)
|
||
tput rmcup 2>/dev/null || true
|
||
trap - RETURN
|
||
|
||
# Display final summary
|
||
echo -e "\n${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
|
||
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
|
||
echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
|
||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")${CL}"
|
||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
||
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
|
||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
|
||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
||
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
|
||
echo -e "${NETWORK}${BOLD}${DGN}IPv4: ${BGN}$NET${CL}"
|
||
echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}$IPV6_METHOD${CL}"
|
||
echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}${ENABLE_FUSE:-no}${CL}"
|
||
[[ "${ENABLE_TUN:-no}" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}"
|
||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")${CL}"
|
||
[[ "${ENABLE_KEYCTL:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}"
|
||
echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}${ENABLE_GPU:-no}${CL}"
|
||
[[ "${PROTECT_CT:-no}" == "yes" || "${PROTECT_CT:-no}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}"
|
||
[[ -n "${CT_TIMEZONE:-}" ]] && echo -e "${INFO}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}"
|
||
[[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}"
|
||
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
|
||
echo -e "${CREATING}${BOLD}${RD}Creating an LXC of ${APP} using the above advanced settings${CL}"
|
||
|
||
# Log settings to file
|
||
log_section "CONTAINER SETTINGS (ADVANCED) - ${APP}"
|
||
log_msg "Application: ${APP}"
|
||
log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
|
||
log_msg "Operating System: $var_os ($var_version)"
|
||
log_msg "Container Type: $([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")"
|
||
log_msg "Container ID: $CT_ID"
|
||
log_msg "Hostname: $HN"
|
||
log_msg "Disk Size: ${DISK_SIZE} GB"
|
||
log_msg "CPU Cores: $CORE_COUNT"
|
||
log_msg "RAM Size: ${RAM_SIZE} MiB"
|
||
log_msg "Bridge: $BRG"
|
||
log_msg "IPv4: $NET"
|
||
log_msg "IPv6: $IPV6_METHOD"
|
||
log_msg "FUSE Support: ${ENABLE_FUSE:-no}"
|
||
log_msg "Nesting: $([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")"
|
||
log_msg "GPU Passthrough: ${ENABLE_GPU:-no}"
|
||
log_msg "Verbose Mode: $VERBOSE"
|
||
log_msg "Session ID: ${SESSION_ID}"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 7: USER INTERFACE & DIAGNOSTICS
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# diagnostics_check()
|
||
#
|
||
# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
|
||
# - Asks user whether to send anonymous diagnostic data (first run only)
|
||
# - Saves DIAGNOSTICS=yes/no in the config file
|
||
# - Reads current diagnostics setting from existing file
|
||
# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out
|
||
# ------------------------------------------------------------------------------
|
||
diagnostics_check() {
|
||
local config_dir="/usr/local/community-scripts"
|
||
local config_file="${config_dir}/diagnostics"
|
||
|
||
mkdir -p "$config_dir"
|
||
|
||
if [[ -f "$config_file" ]]; then
|
||
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' "$config_file") || true
|
||
DIAGNOSTICS="${DIAGNOSTICS:-no}"
|
||
return
|
||
fi
|
||
|
||
local result
|
||
result=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "TELEMETRY & DIAGNOSTICS" \
|
||
--ok-button "Confirm" --cancel-button "Exit" \
|
||
--radiolist "\nHelp improve Community-Scripts by sharing anonymous data.\n\nWhat we collect:\n - Container resources (CPU, RAM, disk), OS & PVE version\n - Application name, install method and status\n\nWhat we DON'T collect:\n - No IP addresses, hostnames, or personal data\n\nYou can change this anytime in the Settings menu.\nPrivacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md\n\nUse SPACE to select, ENTER to confirm." 22 76 2 \
|
||
"yes" "Yes, share anonymous data" OFF \
|
||
"no" "No, opt out" OFF \
|
||
3>&1 1>&2 2>&3) || result="no"
|
||
|
||
DIAGNOSTICS="${result:-no}"
|
||
|
||
cat <<EOF >"$config_file"
|
||
DIAGNOSTICS=${DIAGNOSTICS}
|
||
|
||
# Community-Scripts Telemetry Configuration
|
||
# https://telemetry.community-scripts.org
|
||
#
|
||
# This file stores your telemetry preference.
|
||
# Set DIAGNOSTICS=yes to share anonymous installation data.
|
||
# Set DIAGNOSTICS=no to disable telemetry.
|
||
#
|
||
# You can also change this via the Settings menu during installation.
|
||
#
|
||
# Data collected (when enabled):
|
||
# disk_size, core_count, ram_size, os_type, os_version,
|
||
# nsapp, method, pve_version, status, exit_code
|
||
#
|
||
# No personal data (IPs, hostnames, passwords) is ever collected.
|
||
# Privacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md
|
||
EOF
|
||
}
|
||
|
||
diagnostics_menu() {
|
||
local current="${DIAGNOSTICS:-no}"
|
||
local status_text="DISABLED"
|
||
[[ "$current" == "yes" ]] && status_text="ENABLED"
|
||
|
||
local dialog_text=(
|
||
"Telemetry is currently: ${status_text}\n\n"
|
||
"Anonymous data helps us improve scripts and track issues.\n"
|
||
"No personal data is ever collected.\n\n"
|
||
"More info: https://telemetry.community-scripts.org\n\n"
|
||
"Do you want to ${current:+change this setting}?"
|
||
)
|
||
|
||
if [[ "$current" == "yes" ]]; then
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "TELEMETRY SETTINGS" \
|
||
--yesno "${dialog_text[*]}" 14 64 \
|
||
--yes-button "Disable" --no-button "Keep enabled"; then
|
||
DIAGNOSTICS="no"
|
||
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
|
||
whiptail --msgbox "Telemetry disabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
|
||
fi
|
||
else
|
||
if whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "TELEMETRY SETTINGS" \
|
||
--yesno "${dialog_text[*]}" 14 64 \
|
||
--yes-button "Enable" --no-button "Keep disabled"; then
|
||
DIAGNOSTICS="yes"
|
||
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
|
||
whiptail --msgbox "Telemetry enabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# echo_default()
|
||
#
|
||
# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
|
||
# - Uses icons and formatting for readability
|
||
# - Convert CT_TYPE to description
|
||
# - Also logs settings to log file for debugging
|
||
# ------------------------------------------------------------------------------
|
||
echo_default() {
|
||
CT_TYPE_DESC="Unprivileged"
|
||
if [ "$CT_TYPE" -eq 0 ]; then
|
||
CT_TYPE_DESC="Privileged"
|
||
fi
|
||
echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
|
||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
|
||
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
|
||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
|
||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
||
if [[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]]; then
|
||
echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}Enabled${CL}"
|
||
fi
|
||
if [ "$VERBOSE" == "yes" ]; then
|
||
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
|
||
fi
|
||
echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
|
||
echo -e " "
|
||
|
||
# Log settings to file
|
||
log_section "CONTAINER SETTINGS - ${APP}"
|
||
log_msg "Application: ${APP}"
|
||
log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
|
||
log_msg "Container ID: ${CT_ID}"
|
||
log_msg "Operating System: $var_os ($var_version)"
|
||
log_msg "Container Type: $CT_TYPE_DESC"
|
||
log_msg "Disk Size: ${DISK_SIZE} GB"
|
||
log_msg "CPU Cores: ${CORE_COUNT}"
|
||
log_msg "RAM Size: ${RAM_SIZE} MiB"
|
||
[[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]] && log_msg "GPU Passthrough: Enabled"
|
||
[[ "$VERBOSE" == "yes" ]] && log_msg "Verbose Mode: Enabled"
|
||
log_msg "Session ID: ${SESSION_ID}"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# install_script()
|
||
#
|
||
# - Main entrypoint for installation mode
|
||
# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
|
||
# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
|
||
# - Applies chosen settings and triggers container build
|
||
# ------------------------------------------------------------------------------
|
||
install_script() {
|
||
pve_check
|
||
shell_check
|
||
root_check
|
||
arch_check
|
||
ssh_check
|
||
maxkeys_check
|
||
diagnostics_check
|
||
|
||
if systemctl is-active -q ping-instances.service; then
|
||
systemctl -q stop ping-instances.service
|
||
fi
|
||
|
||
NEXTID=$(pvesh get /cluster/nextid)
|
||
|
||
# Get timezone using timedatectl (Debian 13+ compatible)
|
||
# Fallback to /etc/timezone for older systems
|
||
if command -v timedatectl >/dev/null 2>&1; then
|
||
timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "UTC")
|
||
elif [ -f /etc/timezone ]; then
|
||
timezone=$(cat /etc/timezone)
|
||
else
|
||
timezone="UTC"
|
||
fi
|
||
[[ "${timezone:-}" == Etc/* ]] && timezone="host" # pct doesn't accept Etc/* zones
|
||
|
||
# Show APP Header
|
||
header_info
|
||
|
||
# --- Support CLI argument as direct preset (default, advanced, …) ---
|
||
CHOICE="${mode:-${1:-}}"
|
||
|
||
# If no CLI argument → show whiptail menu
|
||
# Build menu dynamically based on available options
|
||
local appdefaults_option=""
|
||
local settings_option=""
|
||
local menu_items=(
|
||
"1" "Default Install"
|
||
"2" "Advanced Install"
|
||
"3" "User Defaults"
|
||
)
|
||
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
appdefaults_option="4"
|
||
menu_items+=("4" "App Defaults for ${APP}")
|
||
settings_option="5"
|
||
menu_items+=("5" "Settings")
|
||
else
|
||
settings_option="4"
|
||
menu_items+=("4" "Settings")
|
||
fi
|
||
|
||
APPDEFAULTS_OPTION="$appdefaults_option"
|
||
SETTINGS_OPTION="$settings_option"
|
||
|
||
# Main menu loop - allows returning from Settings
|
||
while true; do
|
||
if [ -z "$CHOICE" ]; then
|
||
TMP_CHOICE=$(whiptail \
|
||
--backtitle "Proxmox VE Helper Scripts" \
|
||
--title "Community-Scripts Options" \
|
||
--ok-button "Select" --cancel-button "Exit Script" \
|
||
--notags \
|
||
--menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
|
||
20 60 9 \
|
||
"${menu_items[@]}" \
|
||
--default-item "1" \
|
||
3>&1 1>&2 2>&3) || exit_script
|
||
CHOICE="$TMP_CHOICE"
|
||
fi
|
||
|
||
# --- Main case ---
|
||
local defaults_target=""
|
||
local run_maybe_offer="no"
|
||
case "$CHOICE" in
|
||
1 | default | DEFAULT)
|
||
header_info
|
||
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
|
||
VERBOSE="no"
|
||
METHOD="default"
|
||
base_settings "$VERBOSE"
|
||
echo_default
|
||
defaults_target="$(ensure_global_default_vars_file)"
|
||
break
|
||
;;
|
||
2 | advanced | ADVANCED)
|
||
header_info
|
||
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
|
||
METHOD="advanced"
|
||
base_settings
|
||
advanced_settings
|
||
defaults_target="$(ensure_global_default_vars_file)"
|
||
run_maybe_offer="yes"
|
||
break
|
||
;;
|
||
3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS)
|
||
default_var_settings || {
|
||
msg_error "Failed to apply default.vars"
|
||
exit 110
|
||
}
|
||
defaults_target="/usr/local/community-scripts/default.vars"
|
||
break
|
||
;;
|
||
"$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS)
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
header_info
|
||
echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
|
||
METHOD="appdefaults"
|
||
load_vars_file "$(get_app_defaults_path)" "yes" # Force override script defaults
|
||
base_settings
|
||
echo_default
|
||
defaults_target="$(get_app_defaults_path)"
|
||
break
|
||
else
|
||
msg_error "No App Defaults available for ${APP}"
|
||
exit 111
|
||
fi
|
||
;;
|
||
"$SETTINGS_OPTION" | settings | SETTINGS)
|
||
settings_menu
|
||
# After settings menu, show main menu again
|
||
header_info
|
||
CHOICE=""
|
||
;;
|
||
generated | GENERATED)
|
||
header_info
|
||
echo -e "${DEFAULT}${BOLD}${BL}Using Generated Settings on node $PVEHOST_NAME${CL}"
|
||
VERBOSE="no"
|
||
METHOD="generated"
|
||
base_settings "$VERBOSE"
|
||
echo_default
|
||
break
|
||
;;
|
||
*)
|
||
msg_error "Invalid option: $CHOICE"
|
||
exit 112
|
||
;;
|
||
esac
|
||
done
|
||
|
||
if [[ -n "$defaults_target" ]]; then
|
||
ensure_storage_selection_for_vars_file "$defaults_target"
|
||
fi
|
||
|
||
if [[ "$run_maybe_offer" == "yes" ]]; then
|
||
maybe_offer_save_app_defaults
|
||
fi
|
||
}
|
||
|
||
edit_default_storage() {
|
||
local vf="/usr/local/community-scripts/default.vars"
|
||
|
||
# Ensure file exists
|
||
if [[ ! -f "$vf" ]]; then
|
||
mkdir -p "$(dirname "$vf")"
|
||
touch "$vf"
|
||
fi
|
||
|
||
# Let ensure_storage_selection_for_vars_file handle everything
|
||
ensure_storage_selection_for_vars_file "$vf"
|
||
}
|
||
|
||
settings_menu() {
|
||
while true; do
|
||
local settings_items=(
|
||
"1" "Manage API-Diagnostic Setting"
|
||
"2" "Edit Default.vars"
|
||
)
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
settings_items+=("3" "Edit App.vars for ${APP}")
|
||
settings_items+=("4" "Back to Main Menu")
|
||
else
|
||
settings_items+=("3" "Back to Main Menu")
|
||
fi
|
||
|
||
local choice
|
||
choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "Community-Scripts SETTINGS Menu" \
|
||
--ok-button "Select" --cancel-button "Exit Script" \
|
||
--menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \
|
||
"${settings_items[@]}" \
|
||
3>&1 1>&2 2>&3) || exit_script
|
||
|
||
case "$choice" in
|
||
1) diagnostics_menu ;;
|
||
2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
|
||
3)
|
||
if [ -f "$(get_app_defaults_path)" ]; then
|
||
${EDITOR:-nano} "$(get_app_defaults_path)"
|
||
else
|
||
# Back was selected (no app.vars available)
|
||
return
|
||
fi
|
||
;;
|
||
4)
|
||
# Back to main menu
|
||
return
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# check_container_resources()
|
||
#
|
||
# - Compares host RAM/CPU with required values
|
||
# - Warns if under-provisioned and asks user to continue or abort
|
||
# ------------------------------------------------------------------------------
|
||
check_container_resources() {
|
||
current_ram=$(free -m | awk 'NR==2{print $2}')
|
||
current_cpu=$(nproc)
|
||
|
||
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
|
||
msg_warn "Under-provisioned: Required ${var_cpu} CPU/${var_ram}MB RAM, Current ${current_cpu} CPU/${current_ram}MB RAM"
|
||
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
|
||
echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? <yes/No> "
|
||
read -r prompt </dev/tty
|
||
if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
|
||
msg_error "Aborted: under-provisioned LXC (${current_cpu} CPU/${current_ram}MB RAM < ${var_cpu} CPU/${var_ram}MB RAM)"
|
||
exit 113
|
||
fi
|
||
else
|
||
echo -e ""
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# check_container_storage()
|
||
#
|
||
# - Checks /boot partition usage
|
||
# - Warns if usage >80% and asks user confirmation before proceeding
|
||
# ------------------------------------------------------------------------------
|
||
check_container_storage() {
|
||
total_size=$(df /boot --output=size | tail -n 1)
|
||
local used_size=$(df /boot --output=used | tail -n 1)
|
||
usage=$((100 * used_size / total_size))
|
||
if ((usage > 80)); then
|
||
msg_warn "Storage is dangerously low (${usage}% used on /boot)"
|
||
echo -ne "Continue anyway? <y/N> "
|
||
read -r prompt </dev/tty
|
||
if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
|
||
msg_error "Aborted: storage too low (${usage}% used)"
|
||
exit 114
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ssh_extract_keys_from_file()
|
||
#
|
||
# - Extracts valid SSH public keys from given file
|
||
# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
|
||
# ------------------------------------------------------------------------------
|
||
ssh_extract_keys_from_file() {
|
||
local f="$1"
|
||
[[ -r "$f" ]] || return 0
|
||
tr -d '\r' <"$f" | awk '
|
||
/^[[:space:]]*#/ {next}
|
||
/^[[:space:]]*$/ {next}
|
||
# bare format: type base64 [comment]
|
||
/^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
|
||
# with options: find from first key-type onward
|
||
{
|
||
match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
|
||
if (RSTART>0) { print substr($0, RSTART) }
|
||
}
|
||
'
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ssh_build_choices_from_files()
|
||
#
|
||
# - Builds interactive whiptail checklist of available SSH keys
|
||
# - Generates fingerprint, type and comment for each key
|
||
# ------------------------------------------------------------------------------
|
||
ssh_build_choices_from_files() {
|
||
local -a files=("$@")
|
||
CHOICES=()
|
||
COUNT=0
|
||
MAPFILE="$(mktemp)"
|
||
local id key typ fp cmt base ln=0
|
||
|
||
for f in "${files[@]}"; do
|
||
[[ -f "$f" && -r "$f" ]] || continue
|
||
base="$(basename -- "$f")"
|
||
case "$base" in
|
||
known_hosts | known_hosts.* | config) continue ;;
|
||
id_*) [[ "$f" != *.pub ]] && continue ;;
|
||
esac
|
||
|
||
# map every key in file
|
||
while IFS= read -r key; do
|
||
[[ -n "$key" ]] || continue
|
||
|
||
typ=""
|
||
fp=""
|
||
cmt=""
|
||
# Only the pure key part (without options) is already included in ‘key’.
|
||
read -r _typ _b64 _cmt <<<"$key"
|
||
typ="${_typ:-key}"
|
||
cmt="${_cmt:-}"
|
||
# Fingerprint via ssh-keygen (if available)
|
||
if command -v ssh-keygen >/dev/null 2>&1; then
|
||
fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
|
||
fi
|
||
# Label shorten
|
||
[[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."
|
||
|
||
ln=$((ln + 1))
|
||
COUNT=$((COUNT + 1))
|
||
id="K${COUNT}"
|
||
echo "${id}|${key}" >>"$MAPFILE"
|
||
CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
|
||
done < <(ssh_extract_keys_from_file "$f")
|
||
done
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ssh_discover_default_files()
|
||
#
|
||
# - Scans standard paths for SSH keys
|
||
# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc.
|
||
# ------------------------------------------------------------------------------
|
||
ssh_discover_default_files() {
|
||
local -a cand=()
|
||
shopt -s nullglob
|
||
cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
|
||
cand+=(/root/.ssh/*.pub)
|
||
cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
|
||
shopt -u nullglob
|
||
printf '%s\0' "${cand[@]}"
|
||
}
|
||
|
||
configure_ssh_settings() {
|
||
local step_info="${1:-}"
|
||
local backtitle="Proxmox VE Helper Scripts"
|
||
[[ -n "$step_info" ]] && backtitle="Proxmox VE Helper Scripts [${step_info}]"
|
||
|
||
SSH_KEYS_FILE="$(mktemp)"
|
||
: >"$SSH_KEYS_FILE"
|
||
|
||
IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
|
||
ssh_build_choices_from_files "${_def_files[@]}"
|
||
local default_key_count="$COUNT"
|
||
|
||
local ssh_key_mode
|
||
if [[ "$default_key_count" -gt 0 ]]; then
|
||
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
|
||
"Provision SSH keys for root:" 14 72 4 \
|
||
"found" "Select from detected keys (${default_key_count})" \
|
||
"manual" "Paste a single public key" \
|
||
"folder" "Scan another folder (path or glob)" \
|
||
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
|
||
else
|
||
ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
|
||
"No host keys detected; choose manual/none:" 12 72 2 \
|
||
"manual" "Paste a single public key" \
|
||
"none" "No keys" 3>&1 1>&2 2>&3) || exit_script
|
||
fi
|
||
|
||
case "$ssh_key_mode" in
|
||
found)
|
||
local selection
|
||
selection=$(whiptail --backtitle "$backtitle" --title "SELECT HOST KEYS" \
|
||
--checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
|
||
for tag in $selection; do
|
||
tag="${tag%\"}"
|
||
tag="${tag#\"}"
|
||
local line
|
||
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2- || true)
|
||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
|
||
done
|
||
;;
|
||
manual)
|
||
SSH_AUTHORIZED_KEY="$(whiptail --backtitle "$backtitle" \
|
||
--inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
|
||
[[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
|
||
;;
|
||
folder)
|
||
local glob_path
|
||
glob_path=$(whiptail --backtitle "$backtitle" \
|
||
--inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
|
||
if [[ -n "$glob_path" ]]; then
|
||
shopt -s nullglob
|
||
read -r -a _scan_files <<<"$glob_path"
|
||
shopt -u nullglob
|
||
if [[ "${#_scan_files[@]}" -gt 0 ]]; then
|
||
ssh_build_choices_from_files "${_scan_files[@]}"
|
||
if [[ "$COUNT" -gt 0 ]]; then
|
||
local folder_selection
|
||
folder_selection=$(whiptail --backtitle "$backtitle" --title "SELECT FOLDER KEYS" \
|
||
--checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
|
||
for tag in $folder_selection; do
|
||
tag="${tag%\"}"
|
||
tag="${tag#\"}"
|
||
local line
|
||
line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2- || true)
|
||
[[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
|
||
done
|
||
else
|
||
whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60
|
||
fi
|
||
else
|
||
whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60
|
||
fi
|
||
fi
|
||
;;
|
||
none)
|
||
:
|
||
;;
|
||
esac
|
||
|
||
if [[ -s "$SSH_KEYS_FILE" ]]; then
|
||
sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
|
||
printf '\n' >>"$SSH_KEYS_FILE"
|
||
fi
|
||
|
||
# Always show SSH access dialog - user should be able to enable SSH even without keys
|
||
if (whiptail --backtitle "$backtitle" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
|
||
SSH="yes"
|
||
else
|
||
SSH="no"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# msg_menu()
|
||
#
|
||
# - Displays a numbered menu for update_script() functions
|
||
# - In silent mode (PHS_SILENT=1): auto-selects the default option
|
||
# - In interactive mode: shows menu via read with 10s timeout + default fallback
|
||
# - Usage: CHOICE=$(msg_menu "Title" "tag1" "Description 1" "tag2" "Desc 2" ...)
|
||
# - The first item is always the default
|
||
# - Returns the selected tag to stdout
|
||
# - If no valid selection or timeout, returns the default (first) tag
|
||
# ------------------------------------------------------------------------------
|
||
msg_menu() {
|
||
local title="$1"
|
||
shift
|
||
|
||
# Parse items into parallel arrays: tags[] and descriptions[]
|
||
local -a tags=()
|
||
local -a descs=()
|
||
while [[ $# -ge 2 ]]; do
|
||
tags+=("$1")
|
||
descs+=("$2")
|
||
shift 2
|
||
done
|
||
|
||
local default_tag="${tags[0]}"
|
||
local count=${#tags[@]}
|
||
|
||
# Silent mode: return default immediately
|
||
if [[ -n "${PHS_SILENT+x}" ]] && [[ "${PHS_SILENT}" == "1" ]]; then
|
||
echo "$default_tag"
|
||
return 0
|
||
fi
|
||
|
||
# Display menu to /dev/tty so it doesn't get captured by command substitution
|
||
{
|
||
echo ""
|
||
msg_custom "📋" "${BL}" "${title}"
|
||
echo ""
|
||
for i in "${!tags[@]}"; do
|
||
local marker=" "
|
||
[[ $i -eq 0 ]] && marker="* "
|
||
printf "${TAB3}${marker}%s) %s\n" "${tags[$i]}" "${descs[$i]}"
|
||
done
|
||
echo ""
|
||
} >/dev/tty
|
||
|
||
local selection=""
|
||
read -r -t 10 -p "${TAB3}Select [default=${default_tag}, timeout 10s]: " selection </dev/tty >/dev/tty || true
|
||
|
||
# Validate selection
|
||
if [[ -n "$selection" ]]; then
|
||
for tag in "${tags[@]}"; do
|
||
if [[ "$selection" == "$tag" ]]; then
|
||
echo "$selection"
|
||
return 0
|
||
fi
|
||
done
|
||
msg_warn "Invalid selection '${selection}' - using default: ${default_tag}"
|
||
fi
|
||
|
||
echo "$default_tag"
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# start()
|
||
#
|
||
# - Entry point of script
|
||
# - On Proxmox host: calls install_script
|
||
# - In silent mode: runs update_script with automatic cleanup
|
||
# - Otherwise: shows update/setting menu and runs update_script with cleanup
|
||
# ------------------------------------------------------------------------------
|
||
start() {
|
||
source "$(dirname "${BASH_SOURCE[0]}")/tools.func"
|
||
if command -v pveversion >/dev/null 2>&1; then
|
||
install_script || return 0
|
||
return 0
|
||
elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
|
||
VERBOSE="no"
|
||
set_std_mode
|
||
ensure_profile_loaded
|
||
get_lxc_ip
|
||
update_script
|
||
update_motd_ip
|
||
cleanup_lxc
|
||
else
|
||
CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
|
||
"Support/Update functions for ${APP} LXC. Choose an option:" \
|
||
12 60 3 \
|
||
"1" "YES (Silent Mode)" \
|
||
"2" "YES (Verbose Mode)" \
|
||
"3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
|
||
|
||
case "$CHOICE" in
|
||
1)
|
||
VERBOSE="no"
|
||
set_std_mode
|
||
;;
|
||
2)
|
||
VERBOSE="yes"
|
||
set_std_mode
|
||
;;
|
||
3)
|
||
clear
|
||
exit_script
|
||
exit 0
|
||
;;
|
||
esac
|
||
ensure_profile_loaded
|
||
get_lxc_ip
|
||
update_script
|
||
update_motd_ip
|
||
cleanup_lxc
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 8: CONTAINER CREATION & DEPLOYMENT
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# build_container()
|
||
#
|
||
# - Main function for creating and configuring LXC container
|
||
# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6)
|
||
# - Creates container via pct create with all specified settings
|
||
# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough
|
||
# - Starts container and waits for network connectivity
|
||
# - Installs base packages (curl, sudo, etc.)
|
||
# - Injects SSH keys if configured
|
||
# - Executes <app>-install.sh inside container
|
||
# - Posts installation telemetry to API if diagnostics enabled
|
||
# ------------------------------------------------------------------------------
|
||
build_container() {
|
||
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
|
||
|
||
NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
|
||
|
||
# MAC
|
||
if [[ -n "$MAC" ]]; then
|
||
case "$MAC" in
|
||
,hwaddr=*) NET_STRING+="$MAC" ;;
|
||
*) NET_STRING+=",hwaddr=$MAC" ;;
|
||
esac
|
||
fi
|
||
|
||
# IP (always required, default dhcp)
|
||
# Guard against the literal word "static" being passed as the IP value –
|
||
# that happens when var_net=static is exported without an actual address.
|
||
if [[ "$NET" == "static" || -z "$NET" ]]; then
|
||
msg_warn "NET value '${NET:-empty}' is not a valid IP/CIDR – falling back to dhcp"
|
||
NET="dhcp"
|
||
fi
|
||
NET_STRING+=",ip=${NET}"
|
||
|
||
# Gateway
|
||
if [[ -n "$GATE" ]]; then
|
||
case "$GATE" in
|
||
,gw=) ;;
|
||
,gw=*) NET_STRING+="$GATE" ;;
|
||
*) NET_STRING+=",gw=$GATE" ;;
|
||
esac
|
||
fi
|
||
|
||
# VLAN
|
||
if [[ -n "$VLAN" ]]; then
|
||
case "$VLAN" in
|
||
,tag=*) NET_STRING+="$VLAN" ;;
|
||
*) NET_STRING+=",tag=$VLAN" ;;
|
||
esac
|
||
fi
|
||
|
||
# MTU
|
||
if [[ -n "$MTU" ]]; then
|
||
case "$MTU" in
|
||
,mtu=*) NET_STRING+="$MTU" ;;
|
||
*) NET_STRING+=",mtu=$MTU" ;;
|
||
esac
|
||
fi
|
||
|
||
# IPv6 Handling
|
||
case "$IPV6_METHOD" in
|
||
auto) NET_STRING="$NET_STRING,ip6=auto" ;;
|
||
dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
|
||
static)
|
||
if [[ -n "$IPV6_ADDR" ]]; then
|
||
NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
|
||
[ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
|
||
fi
|
||
;;
|
||
none) ;;
|
||
esac
|
||
|
||
# Build FEATURES string based on container type and user choices
|
||
FEATURES=""
|
||
|
||
# Nesting support (user configurable, default enabled)
|
||
if [ "${ENABLE_NESTING:-1}" == "1" ]; then
|
||
FEATURES="nesting=1"
|
||
fi
|
||
|
||
# Keyctl for unprivileged containers (needed for Docker)
|
||
if [ "$CT_TYPE" == "1" ]; then
|
||
[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
|
||
FEATURES="${FEATURES}keyctl=1"
|
||
fi
|
||
|
||
if [ "$ENABLE_FUSE" == "yes" ]; then
|
||
[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
|
||
FEATURES="${FEATURES}fuse=1"
|
||
fi
|
||
|
||
# Build PCT_OPTIONS as string for export
|
||
local _func_file
|
||
local SCRIPT_DIR
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
if [ "$var_os" == "alpine" ]; then
|
||
_func_file="${SCRIPT_DIR}/alpine-install.func"
|
||
else
|
||
_func_file="${SCRIPT_DIR}/install.func"
|
||
fi
|
||
if [[ -f "$_func_file" ]]; then
|
||
# Concatenate core dependencies + install func so the container has
|
||
# everything via a single `source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"`.
|
||
# install.func has `source $(dirname BASH_SOURCE)/core.func` which
|
||
# resolves to /dev/core.func when sourced from stdin — this avoids that.
|
||
local _core_file="${SCRIPT_DIR}/core.func"
|
||
local _err_file="${SCRIPT_DIR}/error-handler.func"
|
||
local _combined=""
|
||
[[ -f "$_core_file" ]] && _combined+="$(cat "$_core_file")"$'\n'
|
||
[[ -f "$_err_file" ]] && _combined+="$(cat "$_err_file")"$'\n'
|
||
_combined+="$(cat "$_func_file")"
|
||
export FUNCTIONS_FILE_PATH="$_combined"
|
||
else
|
||
local _func_url
|
||
if [ "$var_os" == "alpine" ]; then
|
||
_func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func"
|
||
else
|
||
_func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func"
|
||
fi
|
||
export FUNCTIONS_FILE_PATH="$(curl -fsSL "$_func_url")"
|
||
fi
|
||
if [[ -z "$FUNCTIONS_FILE_PATH" || ${#FUNCTIONS_FILE_PATH} -lt 100 ]]; then
|
||
msg_error "Failed to load install functions"
|
||
exit 115
|
||
fi
|
||
|
||
# Core exports for install.func
|
||
export DIAGNOSTICS="$DIAGNOSTICS"
|
||
export RANDOM_UUID="$RANDOM_UUID"
|
||
export EXECUTION_ID="$EXECUTION_ID"
|
||
export SESSION_ID="$SESSION_ID"
|
||
export CACHER="$APT_CACHER"
|
||
export CACHER_IP="$APT_CACHER_IP"
|
||
export tz="$timezone"
|
||
export APPLICATION="$APP"
|
||
export app="$NSAPP"
|
||
export PASSWORD="$PW"
|
||
export VERBOSE="$VERBOSE"
|
||
export SSH_ROOT="${SSH}"
|
||
export SSH_AUTHORIZED_KEY
|
||
export CTID="$CT_ID"
|
||
export CTTYPE="$CT_TYPE"
|
||
export ENABLE_FUSE="$ENABLE_FUSE"
|
||
export ENABLE_TUN="$ENABLE_TUN"
|
||
export PCT_OSTYPE="$var_os"
|
||
export PCT_OSVERSION="$var_version"
|
||
export PCT_DISK_SIZE="$DISK_SIZE"
|
||
export IPV6_METHOD="$IPV6_METHOD"
|
||
export ENABLE_GPU="$ENABLE_GPU"
|
||
|
||
# DEV_MODE exports (optional, for debugging)
|
||
export BUILD_LOG="$BUILD_LOG"
|
||
export INSTALL_LOG="/root/.install-${SESSION_ID}.log"
|
||
|
||
# Keep host-side logging on BUILD_LOG (not exported — invisible to container)
|
||
# Without this, get_active_logfile() would return INSTALL_LOG (a container path)
|
||
# and all host msg_info/msg_ok/msg_error would write to /root/.install-SESSION.log
|
||
# on the HOST instead of BUILD_LOG, causing incomplete telemetry logs.
|
||
_HOST_LOGFILE="$BUILD_LOG"
|
||
|
||
export dev_mode="${dev_mode:-}"
|
||
export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}"
|
||
export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}"
|
||
export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}"
|
||
export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}"
|
||
export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}"
|
||
export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}"
|
||
export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}"
|
||
|
||
# Build PCT_OPTIONS as multi-line string
|
||
PCT_OPTIONS_STRING=" -hostname $HN"
|
||
|
||
# Only add -tags if TAGS is not empty
|
||
if [ -n "$TAGS" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
-tags $TAGS"
|
||
fi
|
||
|
||
# Only add -features if FEATURES is not empty
|
||
if [ -n "$FEATURES" ]; then
|
||
PCT_OPTIONS_STRING=" -features $FEATURES
|
||
$PCT_OPTIONS_STRING"
|
||
fi
|
||
|
||
# Add searchdomain if specified
|
||
if [ -n "$SD" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
$SD"
|
||
fi
|
||
|
||
# Add nameserver if specified
|
||
if [ -n "$NS" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
$NS"
|
||
fi
|
||
|
||
# Network configuration
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
$NET_STRING
|
||
-onboot 1
|
||
-cores $CORE_COUNT
|
||
-memory $RAM_SIZE
|
||
-unprivileged $CT_TYPE"
|
||
|
||
# Protection flag (if var_protection was set)
|
||
if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
-protection 1"
|
||
fi
|
||
|
||
# Timezone (map Etc/* to "host" as pct doesn't accept them)
|
||
if [ -n "${CT_TIMEZONE:-}" ]; then
|
||
local _pct_timezone="$CT_TIMEZONE"
|
||
[[ "$_pct_timezone" == Etc/* ]] && _pct_timezone="host"
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
-timezone $_pct_timezone"
|
||
fi
|
||
|
||
# Password (already formatted)
|
||
if [ -n "$PW" ]; then
|
||
PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
|
||
$PW"
|
||
fi
|
||
|
||
# Export as string (this works, unlike arrays!)
|
||
export PCT_OPTIONS="$PCT_OPTIONS_STRING"
|
||
export TEMPLATE_STORAGE="${var_template_storage:-}"
|
||
export CONTAINER_STORAGE="${var_container_storage:-}"
|
||
|
||
# Validate storage space only if CONTAINER_STORAGE is already set
|
||
# (Storage selection happens in create_lxc_container for some modes)
|
||
if [[ -n "$CONTAINER_STORAGE" ]]; then
|
||
msg_info "Validating storage space"
|
||
if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then
|
||
local free_space
|
||
free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
|
||
local free_fmt
|
||
free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB")
|
||
msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}"
|
||
exit 214
|
||
fi
|
||
msg_ok "Storage space validated"
|
||
fi
|
||
|
||
create_lxc_container || exit $?
|
||
|
||
# Transition to 'configuring' — container created, now setting up OS/userland
|
||
post_progress_to_api "configuring"
|
||
|
||
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
|
||
|
||
# ============================================================================
|
||
# GPU/USB PASSTHROUGH CONFIGURATION
|
||
# ============================================================================
|
||
|
||
# Check if GPU passthrough is enabled
|
||
# Returns true only if var_gpu is explicitly set to "yes"
|
||
# Can be set via:
|
||
# - Environment variable: var_gpu=yes bash -c "..."
|
||
# - CT script default: var_gpu="${var_gpu:-no}"
|
||
# - Advanced settings wizard
|
||
# - App defaults file: /usr/local/community-scripts/defaults/<app>.vars
|
||
is_gpu_app() {
|
||
[[ "${var_gpu:-no}" == "yes" ]] && return 0
|
||
return 1
|
||
}
|
||
|
||
# Detect all available GPU devices
|
||
detect_gpu_devices() {
|
||
INTEL_DEVICES=()
|
||
AMD_DEVICES=()
|
||
NVIDIA_DEVICES=()
|
||
|
||
# Store PCI info to avoid multiple calls
|
||
# grep returns exit 1 when no match — use || true to prevent ERR trap
|
||
local pci_vga_info
|
||
pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D" || true)
|
||
|
||
# No GPU-related PCI devices at all? Skip silently.
|
||
if [[ -z "$pci_vga_info" ]]; then
|
||
msg_debug "No VGA/Display/3D PCI devices found"
|
||
return 0
|
||
fi
|
||
|
||
# Check for Intel GPU - look for Intel vendor ID [8086]
|
||
if grep -q "\[8086:" <<<"$pci_vga_info"; then
|
||
msg_custom "🎮" "${BL}" "Detected Intel GPU"
|
||
if [[ -d /dev/dri ]]; then
|
||
for d in /dev/dri/renderD* /dev/dri/card*; do
|
||
[[ -e "$d" ]] && INTEL_DEVICES+=("$d")
|
||
done
|
||
fi
|
||
fi
|
||
|
||
# Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
|
||
if grep -qE "\[1002:|\[1022:" <<<"$pci_vga_info"; then
|
||
msg_custom "🎮" "${RD}" "Detected AMD GPU"
|
||
if [[ -d /dev/dri ]]; then
|
||
# Only add if not already claimed by Intel
|
||
if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
|
||
for d in /dev/dri/renderD* /dev/dri/card*; do
|
||
[[ -e "$d" ]] && AMD_DEVICES+=("$d")
|
||
done
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
|
||
if grep -q "\[10de:" <<<"$pci_vga_info"; then
|
||
msg_custom "🎮" "${GN}" "Detected NVIDIA GPU"
|
||
|
||
# Simple passthrough - just bind /dev/nvidia* devices if they exist
|
||
# Only include character devices (-c), skip directories like /dev/nvidia-caps
|
||
for d in /dev/nvidia*; do
|
||
[[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
|
||
done
|
||
# Also check for devices inside /dev/nvidia-caps/ directory
|
||
if [[ -d /dev/nvidia-caps ]]; then
|
||
for d in /dev/nvidia-caps/*; do
|
||
[[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
|
||
done
|
||
fi
|
||
|
||
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
|
||
msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough"
|
||
else
|
||
msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found"
|
||
msg_custom "ℹ️" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)"
|
||
fi
|
||
fi
|
||
|
||
# Debug output
|
||
msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
|
||
msg_debug "AMD devices: ${AMD_DEVICES[*]}"
|
||
msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
|
||
}
|
||
|
||
# Configure USB passthrough for privileged containers
|
||
configure_usb_passthrough() {
|
||
if [[ "$CT_TYPE" != "0" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Configuring automatic USB passthrough (privileged container)"
|
||
cat <<EOF >>"$LXC_CONFIG"
|
||
# Automatic USB passthrough (privileged container)
|
||
lxc.cgroup2.devices.allow: a
|
||
lxc.cap.drop:
|
||
lxc.cgroup2.devices.allow: c 188:* rwm
|
||
lxc.cgroup2.devices.allow: c 189:* rwm
|
||
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
|
||
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
|
||
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
|
||
EOF
|
||
msg_ok "USB passthrough configured"
|
||
}
|
||
|
||
# Configure GPU passthrough
|
||
configure_gpu_passthrough() {
|
||
# Skip if:
|
||
# GPU passthrough is enabled when var_gpu="yes":
|
||
# - Set via environment variable: var_gpu=yes bash -c "..."
|
||
# - Set in CT script: var_gpu="${var_gpu:-no}"
|
||
# - Enabled in advanced_settings wizard
|
||
# - Configured in app defaults file
|
||
if ! is_gpu_app "$APP"; then
|
||
return 0
|
||
fi
|
||
|
||
detect_gpu_devices
|
||
|
||
# Count available GPU types
|
||
local gpu_count=0
|
||
local available_gpus=()
|
||
|
||
if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("INTEL")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("AMD")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
|
||
available_gpus+=("NVIDIA")
|
||
gpu_count=$((gpu_count + 1))
|
||
fi
|
||
|
||
if [[ $gpu_count -eq 0 ]]; then
|
||
msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough"
|
||
return 0
|
||
fi
|
||
|
||
local selected_gpu=""
|
||
|
||
if [[ $gpu_count -eq 1 ]]; then
|
||
# Automatic selection for single GPU
|
||
selected_gpu="${available_gpus[0]}"
|
||
msg_ok "Automatically configuring ${selected_gpu} GPU passthrough"
|
||
else
|
||
# Multiple GPUs - ask user
|
||
echo -e "\n${INFO} Multiple GPU types detected:"
|
||
for gpu in "${available_gpus[@]}"; do
|
||
echo " - $gpu"
|
||
done
|
||
read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu </dev/tty
|
||
selected_gpu="${selected_gpu^^}"
|
||
|
||
# Validate selection
|
||
local valid=0
|
||
for gpu in "${available_gpus[@]}"; do
|
||
[[ "$selected_gpu" == "$gpu" ]] && valid=1
|
||
done
|
||
|
||
if [[ $valid -eq 0 ]]; then
|
||
msg_warn "Invalid selection. Skipping GPU passthrough."
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Apply passthrough configuration based on selection
|
||
local dev_idx=0
|
||
|
||
case "$selected_gpu" in
|
||
INTEL | AMD)
|
||
local devices=()
|
||
[[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
|
||
[[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")
|
||
|
||
# Use pct set to add devices with proper dev0/dev1 format
|
||
# GIDs will be detected and set after container starts
|
||
local dev_index=0
|
||
for dev in "${devices[@]}"; do
|
||
# Add to config using pct set (will be visible in GUI)
|
||
echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
|
||
dev_index=$((dev_index + 1))
|
||
done
|
||
|
||
export GPU_TYPE="$selected_gpu"
|
||
msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)"
|
||
;;
|
||
|
||
NVIDIA)
|
||
if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
|
||
msg_warn "No NVIDIA devices available for passthrough"
|
||
return 0
|
||
fi
|
||
|
||
# Use pct set for NVIDIA devices
|
||
local dev_index=0
|
||
for dev in "${NVIDIA_DEVICES[@]}"; do
|
||
echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
|
||
dev_index=$((dev_index + 1))
|
||
done
|
||
|
||
export GPU_TYPE="NVIDIA"
|
||
msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Additional device passthrough
|
||
configure_additional_devices() {
|
||
# TUN device passthrough
|
||
if [ "$ENABLE_TUN" == "yes" ]; then
|
||
cat <<EOF >>"$LXC_CONFIG"
|
||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||
EOF
|
||
fi
|
||
|
||
# Coral TPU passthrough
|
||
if [[ -e /dev/apex_0 ]]; then
|
||
msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough"
|
||
echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||
fi
|
||
}
|
||
|
||
# Execute pre-start configurations
|
||
configure_usb_passthrough
|
||
configure_gpu_passthrough
|
||
configure_additional_devices
|
||
|
||
# Increase disk size for AMD ROCm runtime (~4GB extra needed)
|
||
if [[ "${GPU_TYPE:-}" == "AMD" ]]; then
|
||
local rocm_extra=4
|
||
local new_disk_size=$((PCT_DISK_SIZE + rocm_extra))
|
||
if pct resize "$CTID" rootfs "${new_disk_size}G" >/dev/null 2>&1; then
|
||
msg_ok "Disk resized ${PCT_DISK_SIZE}GB → ${new_disk_size}GB for ROCm"
|
||
else
|
||
msg_warn "Failed to resize disk for ROCm — installation may fail if space is insufficient"
|
||
fi
|
||
fi
|
||
|
||
# ============================================================================
|
||
# START CONTAINER AND INSTALL USERLAND
|
||
# ============================================================================
|
||
|
||
msg_info "Starting LXC Container"
|
||
pct start "$CTID"
|
||
|
||
# Wait for container to be running
|
||
for i in {1..10}; do
|
||
if pct status "$CTID" | grep -q "status: running"; then
|
||
msg_ok "Started LXC Container"
|
||
break
|
||
fi
|
||
sleep 1
|
||
if [ "$i" -eq 10 ]; then
|
||
local ct_status
|
||
ct_status=$(pct status "$CTID" 2>/dev/null || echo "unknown")
|
||
msg_error "LXC Container did not reach running state (status: ${ct_status})"
|
||
exit 117
|
||
fi
|
||
done
|
||
|
||
# Wait for network (skip for Alpine initially)
|
||
if [ "$var_os" != "alpine" ]; then
|
||
msg_info "Waiting for network in LXC container"
|
||
|
||
# Wait for IP assignment (IPv4 or IPv6)
|
||
local ip_in_lxc=""
|
||
for i in {1..60}; do
|
||
# Try IPv4 first
|
||
ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
|
||
# Fallback to IPv6 if IPv4 not available
|
||
if [ -z "$ip_in_lxc" ]; then
|
||
ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
|
||
fi
|
||
[ -n "$ip_in_lxc" ] && break
|
||
# Progressive backoff: 1s for first 20, 2s for next 20, 3s for last 20
|
||
if [ "$i" -le 20 ]; then
|
||
sleep 1
|
||
elif [ "$i" -le 40 ]; then
|
||
sleep 2
|
||
else
|
||
sleep 3
|
||
fi
|
||
done
|
||
|
||
if [ -z "$ip_in_lxc" ]; then
|
||
msg_error "No IP assigned to CT $CTID after 60 attempts"
|
||
msg_custom "🔧" "${YW}" "Troubleshooting:"
|
||
echo " • Verify bridge ${BRG} exists and has connectivity"
|
||
echo " • Check if DHCP server is reachable (if using DHCP)"
|
||
echo " • Verify static IP configuration (if using static IP)"
|
||
echo " • Check Proxmox firewall rules"
|
||
echo " • If using Tailscale: Disable MagicDNS temporarily"
|
||
exit 118
|
||
fi
|
||
|
||
# Verify basic connectivity (ping test)
|
||
local ping_success=false
|
||
for retry in {1..3}; do
|
||
if pct exec "$CTID" -- ping -c 1 -W 2 1.1.1.1 &>/dev/null ||
|
||
pct exec "$CTID" -- ping -c 1 -W 2 8.8.8.8 &>/dev/null ||
|
||
pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then
|
||
ping_success=true
|
||
break
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
if [ "$ping_success" = false ]; then
|
||
msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed - installation will continue"
|
||
else
|
||
msg_ok "Network in LXC is reachable (ping)"
|
||
fi
|
||
fi
|
||
# Function to get correct GID inside container
|
||
get_container_gid() {
|
||
local group="$1"
|
||
local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
|
||
echo "${gid:-44}" # Default to 44 if not found
|
||
}
|
||
|
||
fix_gpu_gids
|
||
|
||
# Fix Debian 13 LXC template bug where / is owned by nobody:nogroup
|
||
# This must be done from the host as unprivileged containers cannot chown /
|
||
local rootfs
|
||
rootfs=$(pct config "$CTID" | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1 || true)
|
||
if [[ -n "$rootfs" ]]; then
|
||
local mount_point="/var/lib/lxc/${CTID}/rootfs"
|
||
if [[ -d "$mount_point" ]] && [[ "$(stat -c '%U' "$mount_point")" != "root" ]]; then
|
||
chown root:root "$mount_point" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
|
||
# Continue with standard container setup
|
||
msg_info "Customizing LXC Container"
|
||
|
||
# # Install GPU userland if configured
|
||
# if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
|
||
# install_gpu_userland "VAAPI"
|
||
# fi
|
||
|
||
# if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
|
||
# install_gpu_userland "NVIDIA"
|
||
# fi
|
||
|
||
# Disable error trap for entire customization & install phase.
|
||
# All errors are handled explicitly — recovery menu shown on failure.
|
||
# Without this, customization errors (e.g. container stopped during base package
|
||
# install) would trigger error_handler() with a simple "Remove broken container?"
|
||
# prompt instead of the full recovery menu with retry/repair options.
|
||
set +Eeuo pipefail
|
||
trap - ERR
|
||
|
||
local install_exit_code=0
|
||
|
||
# Continue with standard container setup
|
||
if [ "$var_os" == "alpine" ]; then
|
||
sleep 3
|
||
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
|
||
https://dl-cdn.alpinelinux.org/alpine/latest-stable/main
|
||
https://dl-cdn.alpinelinux.org/alpine/latest-stable/community
|
||
EOF'
|
||
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq" >>"$BUILD_LOG" 2>&1 || {
|
||
msg_warn "apk install failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
|
||
local alpine_exit=0
|
||
pct exec "$CTID" -- ash -c '
|
||
ALPINE_MIRRORS="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
|
||
for m in $(printf "%s\n" $ALPINE_MIRRORS | shuf); do
|
||
if wget -q --spider --timeout=2 "http://$m/alpine/latest-stable/main/" 2>/dev/null; then
|
||
echo " Attempting mirror: $m"
|
||
cat <<EOF >/etc/apk/repositories
|
||
http://$m/alpine/latest-stable/main
|
||
http://$m/alpine/latest-stable/community
|
||
EOF
|
||
if apk update >/dev/null 2>&1 && apk add bash newt curl openssh nano mc ncurses jq >/dev/null 2>&1; then
|
||
echo " CDN set to $m: tests passed"
|
||
exit 0
|
||
else
|
||
echo " Mirror $m failed"
|
||
fi
|
||
fi
|
||
done
|
||
exit 2
|
||
' && alpine_exit=0 || alpine_exit=$?
|
||
if [[ $alpine_exit -ne 0 ]]; then
|
||
msg_error "Failed to install base packages in Alpine container"
|
||
install_exit_code=1
|
||
fi
|
||
}
|
||
else
|
||
sleep 3
|
||
LANG=${LANG:-en_US.UTF-8}
|
||
local LANG_ESC="${LANG//./\\.}"
|
||
LANG_ESC="${LANG_ESC//|/\\|}"
|
||
pct exec "$CTID" -- bash -c "sed -i \"/$LANG_ESC/ s/^# //\" /etc/locale.gen"
|
||
pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
|
||
echo LANG=\$locale_line >/etc/default/locale && \
|
||
locale-gen >/dev/null && \
|
||
export LANG=\$locale_line"
|
||
|
||
if [[ -z "${tz:-}" ]]; then
|
||
tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC")
|
||
fi
|
||
[[ "${tz:-}" == Etc/* ]] && tz="UTC" # Normalize Etc/* to UTC for container setup
|
||
|
||
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
|
||
# Set timezone using symlink (Debian 13+ compatible)
|
||
# Create /etc/timezone for backwards compatibility with older scripts
|
||
pct exec "$CTID" -- bash -c "tz='$tz'; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true"
|
||
else
|
||
msg_warn "Skipping timezone setup – zone '$tz' not found in container"
|
||
fi
|
||
|
||
# Detect broken DNS resolver (e.g. Tailscale MagicDNS) and inject public DNS
|
||
if ! pct exec "$CTID" -- bash -c "getent hosts deb.debian.org >/dev/null 2>&1 && getent hosts archive.ubuntu.com >/dev/null 2>&1"; then
|
||
msg_warn "APT repository DNS resolution failed in container, injecting public DNS servers"
|
||
pct exec "$CTID" -- bash -c "echo -e 'nameserver 8.8.8.8\nnameserver 1.1.1.1' >/etc/resolv.conf"
|
||
fi
|
||
|
||
pct exec "$CTID" -- bash -c "apt-get update 2>&1 && apt-get install -y sudo curl mc gnupg2 jq 2>&1" >>"$BUILD_LOG" 2>&1 || {
|
||
local failed_mirror
|
||
failed_mirror=$(pct exec "$CTID" -- bash -c "grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null" 2>/dev/null || echo "unknown")
|
||
msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..."
|
||
local mirror_exit=0
|
||
pct exec "$CTID" -- bash -c '
|
||
APT_BASE="sudo curl mc gnupg2 jq"
|
||
DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")
|
||
|
||
if [ "$DISTRO" = "ubuntu" ]; then
|
||
EU_MIRRORS="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de"
|
||
US_MIRRORS="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu"
|
||
AP_MIRRORS="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au"
|
||
else
|
||
EU_MIRRORS="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
|
||
US_MIRRORS="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
|
||
AP_MIRRORS="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
|
||
fi
|
||
|
||
TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC")
|
||
case "$TZ" in
|
||
Europe/*|Arctic/*) REGIONAL="$EU_MIRRORS"; OTHERS="$US_MIRRORS $AP_MIRRORS" ;;
|
||
America/*) REGIONAL="$US_MIRRORS"; OTHERS="$EU_MIRRORS $AP_MIRRORS" ;;
|
||
Asia/*|Australia/*|Pacific/*) REGIONAL="$AP_MIRRORS"; OTHERS="$EU_MIRRORS $US_MIRRORS" ;;
|
||
*) REGIONAL=""; OTHERS="$EU_MIRRORS $US_MIRRORS $AP_MIRRORS" ;;
|
||
esac
|
||
|
||
echo "Acquire::By-Hash \"no\";" >/etc/apt/apt.conf.d/99no-by-hash
|
||
|
||
try_mirrors() {
|
||
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
|
||
[ -f "$src" ] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${1}/|g; s|deb http[s]*://[^/]*/|deb http://${1}/|g" "$src"
|
||
done
|
||
rm -rf /var/lib/apt/lists/*
|
||
APT_OUT=$(apt-get update 2>&1)
|
||
APT_RC=$?
|
||
if echo "$APT_OUT" | grep -qi "hashsum\|hash sum"; then
|
||
echo " Mirror $1 failed (hash mismatch)"
|
||
return 1
|
||
elif echo "$APT_OUT" | grep -qi "SSL\|certificate"; then
|
||
echo " Mirror $1 failed (SSL/certificate error)"
|
||
return 1
|
||
elif [ $APT_RC -ne 0 ]; then
|
||
echo " Mirror $1 failed (apt-get update error)"
|
||
return 1
|
||
elif apt-get install -y $APT_BASE >/dev/null 2>&1; then
|
||
echo " CDN set to $1: tests passed"
|
||
return 0
|
||
else
|
||
echo " Mirror $1 failed (package install error)"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
scan_reachable() {
|
||
local result=""
|
||
for m in $1; do
|
||
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
|
||
result="$result $m"
|
||
fi
|
||
done
|
||
echo "$result" | xargs
|
||
}
|
||
|
||
# Phase 1: Scan global mirrors first (independent of local CDN issues)
|
||
OTHERS_OK=$(scan_reachable "$OTHERS")
|
||
OTHERS_PICK=$(printf "%s\n" $OTHERS_OK | shuf | head -3 | xargs)
|
||
|
||
for mirror in $OTHERS_PICK; do
|
||
echo " Attempting mirror: $mirror"
|
||
try_mirrors "$mirror" && exit 0
|
||
done
|
||
|
||
# Phase 2: Try primary mirror
|
||
if [ "$DISTRO" = "ubuntu" ]; then
|
||
PRIMARY="archive.ubuntu.com"
|
||
else
|
||
PRIMARY="ftp.debian.org"
|
||
fi
|
||
if timeout 2 bash -c "echo >/dev/tcp/$PRIMARY/80" 2>/dev/null; then
|
||
echo " Attempting mirror: $PRIMARY"
|
||
try_mirrors "$PRIMARY" && exit 0
|
||
fi
|
||
|
||
# Phase 3: Fall back to regional mirrors
|
||
REGIONAL_OK=$(scan_reachable "$REGIONAL")
|
||
REGIONAL_PICK=$(printf "%s\n" $REGIONAL_OK | shuf | head -3 | xargs)
|
||
|
||
for mirror in $REGIONAL_PICK; do
|
||
echo " Attempting mirror: $mirror"
|
||
try_mirrors "$mirror" && exit 0
|
||
done
|
||
|
||
exit 2
|
||
' && mirror_exit=0 || mirror_exit=$?
|
||
if [[ $mirror_exit -eq 2 ]]; then
|
||
msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
|
||
if [[ "$var_os" == "ubuntu" ]]; then
|
||
msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors"
|
||
else
|
||
msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list"
|
||
fi
|
||
local custom_mirror=""
|
||
while true; do
|
||
read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror </dev/tty
|
||
[[ -z "$custom_mirror" ]] && continue
|
||
[[ "$custom_mirror" == "skip" ]] && break
|
||
[[ ! "$custom_mirror" =~ ^[a-zA-Z0-9._-]+$ ]] && {
|
||
msg_warn "Invalid hostname format."
|
||
continue
|
||
}
|
||
pct exec "$CTID" -- bash -c "
|
||
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
|
||
[ -f \"\$src\" ] && sed -i \"s|URIs: http[s]*://[^/]*/|URIs: http://${custom_mirror}/|g; s|deb http[s]*://[^/]*/|deb http://${custom_mirror}/|g\" \"\$src\"
|
||
done
|
||
rm -rf /var/lib/apt/lists/*
|
||
apt-get update >/dev/null 2>&1 && apt-get install -y sudo curl mc gnupg2 jq >/dev/null 2>&1
|
||
" && break
|
||
msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
|
||
done
|
||
if [[ "$custom_mirror" == "skip" ]]; then
|
||
msg_error "apt-get base packages installation failed"
|
||
install_exit_code=1
|
||
fi
|
||
elif [[ $mirror_exit -ne 0 ]]; then
|
||
msg_error "apt-get base packages installation failed"
|
||
install_exit_code=1
|
||
fi
|
||
}
|
||
fi
|
||
|
||
# Only continue with installation if customization succeeded
|
||
if [[ $install_exit_code -eq 0 ]]; then
|
||
msg_ok "Customized LXC Container"
|
||
|
||
# Optional DNS override for retry scenarios (inside LXC, never on host)
|
||
if [[ "${DNS_RETRY_OVERRIDE:-false}" == "true" ]]; then
|
||
msg_info "Applying DNS retry override in LXC (8.8.8.8, 1.1.1.1)"
|
||
pct exec "$CTID" -- bash -c "printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' >/etc/resolv.conf" >/dev/null 2>&1 || true
|
||
msg_ok "DNS override applied in LXC"
|
||
fi
|
||
|
||
# Install SSH keys
|
||
install_ssh_keys_into_ct
|
||
|
||
# Start timer for duration tracking
|
||
start_install_timer
|
||
|
||
# Run application installer
|
||
# Error handling already disabled above (before customization phase)
|
||
|
||
# Signal handlers use this flag to stop the container on abort (SIGHUP/SIGINT/SIGTERM)
|
||
# Without this, SSH disconnects leave the container running as an orphan process
|
||
# that sends "configuring" status AFTER the host already reported "failed"
|
||
export CONTAINER_INSTALLING=true
|
||
|
||
# Use local install script if available, fallback to remote download
|
||
local _local_install="${SCRIPT_DIR}/../install/${var_install}.sh"
|
||
if [[ -f "$_local_install" ]]; then
|
||
lxc-attach -n "$CTID" -- bash -c "$(cat "$_local_install")"
|
||
else
|
||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
|
||
fi
|
||
local lxc_exit=$?
|
||
|
||
unset CONTAINER_INSTALLING
|
||
|
||
# Keep error handling DISABLED during failure detection and recovery
|
||
# Re-enabling it here would cause any pct exec/pull failure to trigger
|
||
# error_handler() on the host, bypassing the recovery menu entirely
|
||
|
||
# Check for error flag file in container (more reliable than lxc-attach exit code)
|
||
if [[ -n "${SESSION_ID:-}" ]]; then
|
||
local error_flag="/root/.install-${SESSION_ID}.failed"
|
||
if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then
|
||
install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1")
|
||
pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
|
||
# Fallback to lxc-attach exit code if no flag file
|
||
if [[ $install_exit_code -eq 0 && ${lxc_exit:-0} -ne 0 ]]; then
|
||
install_exit_code=${lxc_exit:-0}
|
||
fi
|
||
fi # end: if [[ $install_exit_code -eq 0 ]] (customization succeeded)
|
||
|
||
# Installation or customization failed?
|
||
if [[ $install_exit_code -ne 0 ]]; then
|
||
# Prevent job-control signals from suspending the script during recovery.
|
||
# In non-interactive shells (bash -c), background processes (spinner) can
|
||
# trigger terminal-related signals that stop the entire process group.
|
||
# TSTP = Ctrl+Z, TTIN = bg read from tty, TTOU = bg write to tty (tostop)
|
||
trap '' TSTP TTIN TTOU
|
||
|
||
msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})"
|
||
|
||
# Copy install log from container BEFORE API call so get_error_text() can read it
|
||
local build_log_copied=false
|
||
local install_log_copied=false
|
||
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
|
||
|
||
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
|
||
# Create combined log with header
|
||
{
|
||
echo "================================================================================"
|
||
echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
|
||
echo "Container ID: ${CTID}"
|
||
echo "Session ID: ${SESSION_ID}"
|
||
echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo "================================================================================"
|
||
echo ""
|
||
} >"$combined_log"
|
||
|
||
# Append BUILD_LOG (host-side creation log) if it exists
|
||
if [[ -f "${BUILD_LOG}" ]]; then
|
||
{
|
||
echo "================================================================================"
|
||
echo "PHASE 1: CONTAINER CREATION (Host)"
|
||
echo "================================================================================"
|
||
cat "${BUILD_LOG}"
|
||
echo ""
|
||
} >>"$combined_log"
|
||
build_log_copied=true
|
||
fi
|
||
|
||
# Copy and append INSTALL_LOG from container
|
||
local temp_install_log="/tmp/.install-temp-${SESSION_ID}.log"
|
||
if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_install_log" 2>/dev/null; then
|
||
{
|
||
echo "================================================================================"
|
||
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
|
||
echo "================================================================================"
|
||
cat "$temp_install_log"
|
||
echo ""
|
||
} >>"$combined_log"
|
||
rm -f "$temp_install_log"
|
||
install_log_copied=true
|
||
# Point INSTALL_LOG to combined log so get_full_log() finds it
|
||
INSTALL_LOG="$combined_log"
|
||
fi
|
||
fi
|
||
|
||
# Defense-in-depth: Ensure error handling stays disabled during recovery.
|
||
# Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail
|
||
# and trap 'error_handler' ERR. If any code path above called such a function,
|
||
# the grep/sed pipelines below would trigger error_handler on non-match (exit 1).
|
||
set +Eeuo pipefail
|
||
trap - ERR
|
||
|
||
# --- Exit code 1 subclassification: analyze logs BEFORE telemetry call ---
|
||
# Exit code 1 is generic ("General error"). Analyze logs to determine the
|
||
# real error category so telemetry gets a useful classification instead of "shell".
|
||
local is_oom=false
|
||
local is_network_issue=false
|
||
local is_apt_issue=false
|
||
local is_cmd_not_found=false
|
||
local is_disk_full=false
|
||
|
||
if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then
|
||
if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then
|
||
is_apt_issue=true
|
||
fi
|
||
if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then
|
||
is_oom=true
|
||
fi
|
||
if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then
|
||
is_network_issue=true
|
||
fi
|
||
if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then
|
||
is_cmd_not_found=true
|
||
fi
|
||
if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
|
||
is_disk_full=true
|
||
fi
|
||
fi
|
||
|
||
# Set override for categorize_error() so telemetry gets the real category
|
||
if [[ "$is_apt_issue" == true ]]; then
|
||
export ERROR_CATEGORY_OVERRIDE="dependency"
|
||
elif [[ "$is_oom" == true ]]; then
|
||
export ERROR_CATEGORY_OVERRIDE="resource"
|
||
elif [[ "$is_network_issue" == true ]]; then
|
||
export ERROR_CATEGORY_OVERRIDE="network"
|
||
elif [[ "$is_disk_full" == true ]]; then
|
||
export ERROR_CATEGORY_OVERRIDE="storage"
|
||
elif [[ "$is_cmd_not_found" == true ]]; then
|
||
export ERROR_CATEGORY_OVERRIDE="dependency"
|
||
fi
|
||
|
||
# Report failure to telemetry API (now with log available on host)
|
||
# NOTE: Do NOT use msg_info/spinner here — the background spinner process
|
||
# causes SIGTSTP in non-interactive shells (bash -c "$(curl ...)"), which
|
||
# stops the entire process group and prevents the recovery dialog from appearing.
|
||
$STD echo -e "${TAB}⏳ Reporting failure to telemetry..."
|
||
post_update_to_api "failed" "$install_exit_code"
|
||
$STD echo -e "${TAB}${CM:-✔} Failure reported"
|
||
|
||
# Show combined log location
|
||
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
|
||
msg_custom "📋" "${YW}" "Installation log: ${combined_log}"
|
||
fi
|
||
|
||
# Dev mode: Keep container or open breakpoint shell
|
||
if [[ "${DEV_MODE_KEEP:-false}" == "true" ]]; then
|
||
msg_dev "Keep mode active - container ${CTID} preserved"
|
||
return 0
|
||
elif [[ "${DEV_MODE_BREAKPOINT:-false}" == "true" ]]; then
|
||
msg_dev "Breakpoint mode - opening shell in container ${CTID}"
|
||
echo -e "${YW}Type 'exit' to return to host${CL}"
|
||
pct enter "$CTID"
|
||
echo ""
|
||
echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}"
|
||
if read -r response </dev/tty && [[ "$response" =~ ^[Yy]$ ]]; then
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
msg_ok "Container ${CTID} removed"
|
||
else
|
||
msg_dev "Container ${CTID} kept for debugging"
|
||
fi
|
||
exit $install_exit_code
|
||
fi
|
||
|
||
# Prompt user for cleanup with 60s timeout
|
||
echo ""
|
||
|
||
# Extend error detection for non-exit-1 codes (exit 1 was already analyzed above)
|
||
# The is_* flags were set above for exit code 1 log analysis; here we add
|
||
# exit-code-specific detections for other codes.
|
||
local error_explanation=""
|
||
if declare -f explain_exit_code >/dev/null 2>&1; then
|
||
error_explanation="$(explain_exit_code "$install_exit_code")"
|
||
fi
|
||
|
||
# OOM detection: exit codes 134 (SIGABRT/heap), 137 (SIGKILL/OOM), 243 (Node.js heap)
|
||
if [[ $install_exit_code -eq 134 || $install_exit_code -eq 137 || $install_exit_code -eq 243 ]]; then
|
||
is_oom=true
|
||
fi
|
||
|
||
# APT/DPKG detection: exit codes 100-102 (APT), 255 (DPKG with log evidence)
|
||
case "$install_exit_code" in
|
||
100 | 101 | 102) is_apt_issue=true ;;
|
||
255)
|
||
if [[ -f "$combined_log" ]] && grep -qiE 'dpkg|apt-get|apt\.conf|broken packages|unmet dependencies|E: Sub-process|E: Failed' "$combined_log"; then
|
||
is_apt_issue=true
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
# Disk full / ENOSPC detection: errno -28 (ENOSPC), exit 228 (custom handler), exit 23 (curl write error)
|
||
if [[ $install_exit_code -eq 228 || $install_exit_code -eq 23 ]]; then
|
||
is_disk_full=true
|
||
fi
|
||
if [[ -f "$combined_log" ]] && grep -qiE 'ENOSPC|no space left on device|No space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
|
||
is_disk_full=true
|
||
fi
|
||
|
||
# Command not found detection
|
||
if [[ $install_exit_code -eq 127 ]]; then
|
||
is_cmd_not_found=true
|
||
fi
|
||
|
||
# Network-related detection (curl/apt/git fetch failures and transient network issues)
|
||
case "$install_exit_code" in
|
||
6 | 7 | 22 | 28 | 35 | 52 | 56 | 57 | 75 | 78) is_network_issue=true ;;
|
||
100)
|
||
# APT can fail due to network (Failed to fetch)
|
||
if [[ -f "$combined_log" ]] && grep -qiE 'Failed to fetch|Could not resolve|Connection failed|Network is unreachable|Temporary failure resolving' "$combined_log"; then
|
||
is_network_issue=true
|
||
fi
|
||
;;
|
||
128)
|
||
if [[ -f "$combined_log" ]] && grep -qiE 'RPC failed|early EOF|fetch-pack|HTTP/2 stream|Could not resolve host|Temporary failure resolving|Failed to fetch|Connection reset|Network is unreachable' "$combined_log"; then
|
||
is_network_issue=true
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
# Show error explanation if available
|
||
if [[ -n "$error_explanation" ]]; then
|
||
echo -e "${TAB}${RD}Error: ${error_explanation}${CL}"
|
||
echo ""
|
||
fi
|
||
|
||
# Show specific hints for known error types
|
||
if [[ $install_exit_code -eq 10 ]]; then
|
||
echo -e "${TAB}${INFO} This error usually means the container needs ${GN}privileged${CL} mode or Docker/nesting support."
|
||
echo -e "${TAB}${INFO} Recreate with: Advanced Install → Container Type: ${GN}Privileged${CL}"
|
||
echo ""
|
||
fi
|
||
|
||
if [[ $install_exit_code -eq 125 || $install_exit_code -eq 126 ]]; then
|
||
echo -e "${TAB}${INFO} The command exists but cannot be executed. This may be a ${GN}permission${CL} issue."
|
||
echo -e "${TAB}${INFO} If using Docker, ensure the container is ${GN}privileged${CL} or has correct permissions."
|
||
echo ""
|
||
fi
|
||
|
||
if [[ "$is_disk_full" == true ]]; then
|
||
echo -e "${TAB}${INFO} The container ran out of disk space during installation (${GN}ENOSPC${CL})."
|
||
echo -e "${TAB}${INFO} Current disk size: ${GN}${DISK_SIZE} GB${CL}. A rebuild with doubled disk may resolve this."
|
||
echo ""
|
||
fi
|
||
|
||
if [[ "$is_cmd_not_found" == true ]]; then
|
||
local missing_cmd=""
|
||
if [[ -f "$combined_log" ]]; then
|
||
missing_cmd=$(grep -oiE '[a-zA-Z0-9_.-]+: command not found' "$combined_log" 2>/dev/null | tail -1 | sed 's/: command not found//') || true
|
||
fi
|
||
if [[ -n "$missing_cmd" ]]; then
|
||
echo -e "${TAB}${INFO} Missing command: ${GN}${missing_cmd}${CL}"
|
||
fi
|
||
echo ""
|
||
fi
|
||
|
||
# Build recovery menu based on error type
|
||
echo -e "${YW}What would you like to do?${CL}"
|
||
echo ""
|
||
echo -e " ${GN}1)${CL} Remove container and exit"
|
||
echo -e " ${GN}2)${CL} Keep container for debugging"
|
||
echo -e " ${GN}3)${CL} Retry with verbose mode (full rebuild)"
|
||
|
||
local next_option=4
|
||
local APT_OPTION="" OOM_OPTION="" DNS_OPTION="" DISK_OPTION=""
|
||
|
||
if [[ "$is_apt_issue" == true ]]; then
|
||
if [[ "$var_os" == "alpine" ]]; then
|
||
echo -e " ${GN}${next_option})${CL} Repair APK state and re-run install (in-place)"
|
||
else
|
||
echo -e " ${GN}${next_option})${CL} Repair APT/DPKG state and re-run install (in-place)"
|
||
fi
|
||
APT_OPTION=$next_option
|
||
next_option=$((next_option + 1))
|
||
fi
|
||
|
||
if [[ "$is_oom" == true ]]; then
|
||
local recovery_attempt="${RECOVERY_ATTEMPT:-0}"
|
||
if [[ $recovery_attempt -lt 2 ]]; then
|
||
local new_ram=$((RAM_SIZE * 2))
|
||
local new_cpu=$((CORE_COUNT * 2))
|
||
echo -e " ${GN}${next_option})${CL} Retry with more resources (RAM: ${RAM_SIZE}→${new_ram} MiB, CPU: ${CORE_COUNT}→${new_cpu} cores)"
|
||
OOM_OPTION=$next_option
|
||
next_option=$((next_option + 1))
|
||
else
|
||
echo -e " ${DGN}-)${CL} ${DGN}OOM retry exhausted (already retried ${recovery_attempt}x)${CL}"
|
||
fi
|
||
fi
|
||
|
||
if [[ "$is_disk_full" == true ]]; then
|
||
local disk_recovery_attempt="${DISK_RECOVERY_ATTEMPT:-0}"
|
||
if [[ $disk_recovery_attempt -lt 2 ]]; then
|
||
local new_disk=$((DISK_SIZE * 2))
|
||
echo -e " ${GN}${next_option})${CL} Retry with more disk space (Disk: ${DISK_SIZE}→${new_disk} GB)"
|
||
DISK_OPTION=$next_option
|
||
next_option=$((next_option + 1))
|
||
else
|
||
echo -e " ${DGN}-)${CL} ${DGN}Disk resize retry exhausted (already retried ${disk_recovery_attempt}x)${CL}"
|
||
fi
|
||
fi
|
||
|
||
if [[ "$is_network_issue" == true ]]; then
|
||
echo -e " ${GN}${next_option})${CL} Retry with DNS override in LXC (8.8.8.8 / 1.1.1.1)"
|
||
DNS_OPTION=$next_option
|
||
next_option=$((next_option + 1))
|
||
fi
|
||
|
||
local max_option=$((next_option - 1))
|
||
|
||
echo ""
|
||
echo -en "${YW}Select option [1-${max_option}] (default: 1, auto-remove in 60s): ${CL}"
|
||
|
||
local response=""
|
||
if read -t 60 -r response; then
|
||
case "${response:-1}" in
|
||
1)
|
||
# Remove container
|
||
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
|
||
;;
|
||
2)
|
||
echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}"
|
||
# Dev mode: Setup MOTD/SSH for debugging access to broken container
|
||
if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then
|
||
echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}"
|
||
if pct exec "$CTID" -- bash -c "
|
||
source /tmp/scripts/core/install.func
|
||
declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true
|
||
" >/dev/null 2>&1; then
|
||
local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
|
||
echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}"
|
||
fi
|
||
fi
|
||
exit $install_exit_code
|
||
;;
|
||
3)
|
||
# Retry with verbose mode (full rebuild)
|
||
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild...${CL}"
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
|
||
echo ""
|
||
# Get new container ID
|
||
local old_ctid="$CTID"
|
||
export CTID=$(get_valid_container_id "$CTID")
|
||
export VERBOSE="yes"
|
||
export var_verbose="yes"
|
||
|
||
# Show rebuild summary
|
||
echo -e "${YW}Rebuilding with preserved settings:${CL}"
|
||
echo -e " Container ID: ${old_ctid} → ${CTID}"
|
||
echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB"
|
||
echo -e " Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
|
||
echo -e " Verbose: ${GN}enabled${CL}"
|
||
echo ""
|
||
msg_info "Restarting installation..."
|
||
# Re-run build_container
|
||
build_container
|
||
return $?
|
||
;;
|
||
*)
|
||
# Handle dynamic smart recovery options via named option variables
|
||
local handled=false
|
||
|
||
if [[ -n "${APT_OPTION}" && "${response}" == "${APT_OPTION}" ]]; then
|
||
# Package manager in-place repair: fix broken state and re-run install script
|
||
handled=true
|
||
if [[ "$var_os" == "alpine" ]]; then
|
||
echo -e "\n${TAB}${HOLD}${YW}Repairing APK state in container ${CTID}...${CL}"
|
||
pct exec "$CTID" -- ash -c "
|
||
apk fix 2>/dev/null || true
|
||
apk cache clean 2>/dev/null || true
|
||
apk update 2>/dev/null || true
|
||
" >/dev/null 2>&1 || true
|
||
echo -e "${BFR}${CM}${GN}APK state repaired in container ${CTID}${CL}"
|
||
else
|
||
echo -e "\n${TAB}${HOLD}${YW}Repairing APT/DPKG state in container ${CTID}...${CL}"
|
||
pct exec "$CTID" -- bash -c "
|
||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a 2>/dev/null || true
|
||
apt-get -f install -y 2>/dev/null || true
|
||
apt-get clean 2>/dev/null
|
||
apt-get update 2>/dev/null || true
|
||
" >/dev/null 2>&1 || true
|
||
echo -e "${BFR}${CM}${GN}APT/DPKG state repaired in container ${CTID}${CL}"
|
||
fi
|
||
echo ""
|
||
export VERBOSE="yes"
|
||
export var_verbose="yes"
|
||
|
||
echo -e "${YW}Re-running installation in existing container ${CTID}:${CL}"
|
||
echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB"
|
||
echo -e " Verbose: ${GN}enabled${CL}"
|
||
echo ""
|
||
msg_info "Re-running installation script..."
|
||
|
||
# Re-run install script in existing container (don't destroy/recreate)
|
||
set +Eeuo pipefail
|
||
trap - ERR
|
||
if [[ -f "$_local_install" ]]; then
|
||
lxc-attach -n "$CTID" -- bash -c "$(cat "$_local_install")"
|
||
else
|
||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
|
||
fi
|
||
local apt_retry_exit=$?
|
||
set -Eeuo pipefail
|
||
trap 'error_handler' ERR
|
||
|
||
# Check for error flag from retry
|
||
local apt_retry_code=0
|
||
if [[ -n "${SESSION_ID:-}" ]]; then
|
||
local retry_error_flag="/root/.install-${SESSION_ID}.failed"
|
||
if pct exec "$CTID" -- test -f "$retry_error_flag" 2>/dev/null; then
|
||
apt_retry_code=$(pct exec "$CTID" -- cat "$retry_error_flag" 2>/dev/null || echo "1")
|
||
pct exec "$CTID" -- rm -f "$retry_error_flag" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
|
||
if [[ $apt_retry_code -eq 0 && $apt_retry_exit -ne 0 ]]; then
|
||
apt_retry_code=$apt_retry_exit
|
||
fi
|
||
|
||
if [[ $apt_retry_code -eq 0 ]]; then
|
||
msg_ok "Installation completed successfully after APT repair!"
|
||
INSTALL_COMPLETE=true
|
||
post_update_to_api "done" "0" "force"
|
||
return 0
|
||
else
|
||
msg_error "Installation still failed after APT repair (exit code: ${apt_retry_code})"
|
||
install_exit_code=$apt_retry_code
|
||
fi
|
||
fi
|
||
|
||
if [[ -n "${OOM_OPTION}" && "${response}" == "${OOM_OPTION}" ]]; then
|
||
# Retry with doubled resources
|
||
handled=true
|
||
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more resources...${CL}"
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
|
||
echo ""
|
||
local old_ctid="$CTID"
|
||
local old_ram="$RAM_SIZE"
|
||
local old_cpu="$CORE_COUNT"
|
||
export CTID=$(get_valid_container_id "$CTID")
|
||
export RAM_SIZE=$((RAM_SIZE * 2))
|
||
export CORE_COUNT=$((CORE_COUNT * 2))
|
||
export var_ram="$RAM_SIZE"
|
||
export var_cpu="$CORE_COUNT"
|
||
export VERBOSE="yes"
|
||
export var_verbose="yes"
|
||
export RECOVERY_ATTEMPT=$((${RECOVERY_ATTEMPT:-0} + 1))
|
||
|
||
echo -e "${YW}Rebuilding with increased resources (attempt ${RECOVERY_ATTEMPT}/2):${CL}"
|
||
echo -e " Container ID: ${old_ctid} → ${CTID}"
|
||
echo -e " RAM: ${old_ram} → ${GN}${RAM_SIZE}${CL} MiB (x2)"
|
||
echo -e " CPU: ${old_cpu} → ${GN}${CORE_COUNT}${CL} cores (x2)"
|
||
echo -e " Disk: ${DISK_SIZE} GB | Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
|
||
echo -e " Verbose: ${GN}enabled${CL}"
|
||
echo ""
|
||
msg_info "Restarting installation..."
|
||
build_container
|
||
return $?
|
||
fi
|
||
|
||
if [[ -n "${DISK_OPTION}" && "${response}" == "${DISK_OPTION}" ]]; then
|
||
# Retry with doubled disk size
|
||
handled=true
|
||
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more disk space...${CL}"
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
|
||
echo ""
|
||
local old_ctid="$CTID"
|
||
local old_disk="$DISK_SIZE"
|
||
export CTID=$(get_valid_container_id "$CTID")
|
||
export DISK_SIZE=$((DISK_SIZE * 2))
|
||
export var_disk="$DISK_SIZE"
|
||
export VERBOSE="yes"
|
||
export var_verbose="yes"
|
||
export DISK_RECOVERY_ATTEMPT=$((${DISK_RECOVERY_ATTEMPT:-0} + 1))
|
||
|
||
echo -e "${YW}Rebuilding with increased disk space (attempt ${DISK_RECOVERY_ATTEMPT}/2):${CL}"
|
||
echo -e " Container ID: ${old_ctid} → ${CTID}"
|
||
echo -e " Disk: ${old_disk} → ${GN}${DISK_SIZE}${CL} GB (x2)"
|
||
echo -e " RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores"
|
||
echo -e " Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
|
||
echo -e " Verbose: ${GN}enabled${CL}"
|
||
echo ""
|
||
msg_info "Restarting installation..."
|
||
build_container
|
||
return $?
|
||
fi
|
||
|
||
if [[ -n "${DNS_OPTION}" && "${response}" == "${DNS_OPTION}" ]]; then
|
||
# Retry with DNS override in LXC
|
||
handled=true
|
||
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with DNS override...${CL}"
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
|
||
echo ""
|
||
local old_ctid="$CTID"
|
||
export CTID=$(get_valid_container_id "$CTID")
|
||
export DNS_RETRY_OVERRIDE="true"
|
||
export VERBOSE="yes"
|
||
export var_verbose="yes"
|
||
|
||
echo -e "${YW}Rebuilding with DNS override in LXC:${CL}"
|
||
echo -e " Container ID: ${old_ctid} → ${CTID}"
|
||
echo -e " DNS: ${GN}8.8.8.8, 1.1.1.1${CL} (inside LXC only)"
|
||
echo -e " Verbose: ${GN}enabled${CL}"
|
||
echo ""
|
||
msg_info "Restarting installation..."
|
||
build_container
|
||
return $?
|
||
fi
|
||
|
||
if [[ "$handled" == false ]]; then
|
||
echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}"
|
||
exit $install_exit_code
|
||
fi
|
||
;;
|
||
esac
|
||
else
|
||
# Timeout - auto-remove
|
||
echo ""
|
||
msg_info "No response - removing container ${CTID}"
|
||
pct stop "$CTID" &>/dev/null || true
|
||
pct destroy "$CTID" &>/dev/null || true
|
||
msg_ok "Container ${CTID} removed"
|
||
fi
|
||
|
||
# Force one final status update attempt after cleanup
|
||
# This ensures status is updated even if the first attempt failed (e.g., HTTP 400)
|
||
$STD echo -e "${TAB}⏳ Finalizing telemetry report..."
|
||
post_update_to_api "failed" "$install_exit_code" "force"
|
||
$STD echo -e "${TAB}${CM:-✔} Telemetry finalized"
|
||
|
||
# Restore default job-control signal handling before exit
|
||
trap - TSTP TTIN TTOU
|
||
exit $install_exit_code
|
||
fi
|
||
|
||
# Re-enable error handling after successful install or recovery menu completion
|
||
set -Eeuo pipefail
|
||
trap 'error_handler' ERR
|
||
}
|
||
|
||
destroy_lxc() {
|
||
if [[ -z "$CT_ID" ]]; then
|
||
msg_error "No CT_ID found. Nothing to remove."
|
||
return 65
|
||
fi
|
||
|
||
# Abort on Ctrl-C / Ctrl-D / ESC
|
||
trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT
|
||
|
||
local prompt
|
||
if ! read -rp "Remove this Container? <y/N> " prompt </dev/tty; then
|
||
# read returns non-zero on Ctrl-D/ESC
|
||
msg_error "Aborted input (Ctrl-D/ESC)"
|
||
return 130
|
||
fi
|
||
|
||
case "${prompt,,}" in
|
||
y | yes)
|
||
if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
|
||
msg_ok "Removed Container $CT_ID"
|
||
else
|
||
msg_error "Failed to remove Container $CT_ID"
|
||
return 1
|
||
fi
|
||
;;
|
||
"" | n | no)
|
||
msg_custom "ℹ️" "${BL}" "Container was not removed."
|
||
;;
|
||
*)
|
||
msg_warn "Invalid response. Container was not removed."
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Storage discovery / selection helpers
|
||
# ------------------------------------------------------------------------------
|
||
resolve_storage_preselect() {
|
||
local class="$1" preselect="$2" required_content=""
|
||
case "$class" in
|
||
template) required_content="vztmpl" ;;
|
||
container) required_content="rootdir" ;;
|
||
*) return 65 ;;
|
||
esac
|
||
[[ -z "$preselect" ]] && return 1
|
||
if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then
|
||
msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)"
|
||
return 238
|
||
fi
|
||
|
||
local line total used free
|
||
line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
|
||
if [[ -z "$line" ]]; then
|
||
STORAGE_INFO="n/a"
|
||
else
|
||
total="$(awk '{print $4}' <<<"$line")"
|
||
used="$(awk '{print $5}' <<<"$line")"
|
||
free="$(awk '{print $6}' <<<"$line")"
|
||
local total_h used_h free_h
|
||
if command -v numfmt >/dev/null 2>&1; then
|
||
total_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
|
||
used_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
|
||
free_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
|
||
STORAGE_INFO="Free: ${free_h} Used: ${used_h}"
|
||
else
|
||
STORAGE_INFO="Free: ${free} Used: ${used}"
|
||
fi
|
||
fi
|
||
STORAGE_RESULT="$preselect"
|
||
return 0
|
||
}
|
||
|
||
fix_gpu_gids() {
|
||
if [[ -z "${GPU_TYPE:-}" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
msg_info "Detecting and setting correct GPU group IDs"
|
||
|
||
# Get actual GIDs from container
|
||
local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
|
||
local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
|
||
|
||
# Create groups if they don't exist
|
||
if [[ -z "$video_gid" ]]; then
|
||
pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>&1
|
||
video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
|
||
[[ -z "$video_gid" ]] && video_gid="44"
|
||
fi
|
||
|
||
if [[ -z "$render_gid" ]]; then
|
||
pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>&1
|
||
render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
|
||
[[ -z "$render_gid" ]] && render_gid="104"
|
||
fi
|
||
|
||
# Stop container to update config
|
||
pct stop "$CTID" >/dev/null 2>&1
|
||
sleep 1
|
||
|
||
# Validate GIDs are numeric before sed
|
||
[[ "$render_gid" =~ ^[0-9]+$ ]] || render_gid="104"
|
||
[[ "$video_gid" =~ ^[0-9]+$ ]] || video_gid="44"
|
||
|
||
# Update dev entries with correct GIDs
|
||
sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG"
|
||
sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG"
|
||
|
||
# Restart container
|
||
pct start "$CTID" >/dev/null 2>&1
|
||
sleep 2
|
||
|
||
msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})"
|
||
|
||
# For privileged containers: also fix permissions inside container
|
||
if [[ "$CT_TYPE" == "0" ]]; then
|
||
pct exec "$CTID" -- sh -c "
|
||
if [ -d /dev/dri ]; then
|
||
for dev in /dev/dri/*; do
|
||
if [ -e \"\$dev\" ]; then
|
||
case \"\$dev\" in
|
||
*renderD*) chgrp ${render_gid} \"\$dev\" 2>/dev/null || true ;;
|
||
*) chgrp ${video_gid} \"\$dev\" 2>/dev/null || true ;;
|
||
esac
|
||
chmod 660 \"\$dev\" 2>/dev/null || true
|
||
fi
|
||
done
|
||
fi
|
||
" >/dev/null 2>&1
|
||
fi
|
||
}
|
||
|
||
check_storage_support() {
|
||
local CONTENT="$1" VALID=0
|
||
while IFS= read -r line; do
|
||
local STORAGE_NAME
|
||
STORAGE_NAME=$(awk '{print $1}' <<<"$line")
|
||
[[ -n "$STORAGE_NAME" ]] && VALID=1
|
||
done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
|
||
[[ $VALID -eq 1 ]]
|
||
}
|
||
|
||
select_storage() {
|
||
local CLASS=$1 CONTENT CONTENT_LABEL
|
||
case $CLASS in
|
||
container)
|
||
CONTENT='rootdir'
|
||
CONTENT_LABEL='Container'
|
||
;;
|
||
template)
|
||
CONTENT='vztmpl'
|
||
CONTENT_LABEL='Container template'
|
||
;;
|
||
iso)
|
||
CONTENT='iso'
|
||
CONTENT_LABEL='ISO image'
|
||
;;
|
||
images)
|
||
CONTENT='images'
|
||
CONTENT_LABEL='VM Disk image'
|
||
;;
|
||
backup)
|
||
CONTENT='backup'
|
||
CONTENT_LABEL='Backup'
|
||
;;
|
||
snippets)
|
||
CONTENT='snippets'
|
||
CONTENT_LABEL='Snippets'
|
||
;;
|
||
*)
|
||
msg_error "Invalid storage class '$CLASS'"
|
||
return 65
|
||
;;
|
||
esac
|
||
|
||
declare -A STORAGE_MAP
|
||
local -a MENU=()
|
||
local COL_WIDTH=0
|
||
|
||
while read -r TAG TYPE _ TOTAL USED FREE _; do
|
||
[[ -n "$TAG" && -n "$TYPE" ]] || continue
|
||
local DISPLAY="${TAG} (${TYPE})"
|
||
local USED_FMT=$(numfmt --to=iec --from-unit=1024 --format %.1f <<<"$USED")
|
||
local FREE_FMT=$(numfmt --to=iec --from-unit=1024 --format %.1f <<<"$FREE")
|
||
local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
|
||
STORAGE_MAP["$DISPLAY"]="$TAG"
|
||
MENU+=("$DISPLAY" "$INFO" "OFF")
|
||
((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
|
||
done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
|
||
|
||
if [[ ${#MENU[@]} -eq 0 ]]; then
|
||
msg_error "No storage found for content type '$CONTENT'."
|
||
return 2
|
||
fi
|
||
|
||
if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
|
||
STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
|
||
STORAGE_INFO="${MENU[1]}"
|
||
return 0
|
||
fi
|
||
|
||
local WIDTH=$((COL_WIDTH + 42))
|
||
while true; do
|
||
local DISPLAY_SELECTED
|
||
DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||
--title "Storage Pools" \
|
||
--radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
|
||
16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }
|
||
|
||
DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
|
||
if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
|
||
whiptail --msgbox "No valid storage selected. Please try again." 8 58
|
||
continue
|
||
fi
|
||
STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
|
||
for ((i = 0; i < ${#MENU[@]}; i += 3)); do
|
||
if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
|
||
STORAGE_INFO="${MENU[$i + 1]}"
|
||
break
|
||
fi
|
||
done
|
||
|
||
# Validate storage space for container storage
|
||
if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then
|
||
validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes"
|
||
# Continue even if validation fails - user was warned
|
||
fi
|
||
|
||
return 0
|
||
done
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# validate_storage_space()
|
||
#
|
||
# - Validates if storage has enough free space for container
|
||
# - Takes storage name and required size in GB
|
||
# - Returns 0 if enough space, 1 if not enough, 2 if storage unavailable
|
||
# - Can optionally show whiptail warning
|
||
# - Handles all storage types: dir, lvm, lvmthin, zfs, nfs, cifs, etc.
|
||
# ------------------------------------------------------------------------------
|
||
validate_storage_space() {
|
||
local storage="$1"
|
||
local required_gb="${2:-8}"
|
||
local show_dialog="${3:-no}"
|
||
|
||
# Get full storage line from pvesm status
|
||
local storage_line
|
||
storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}')
|
||
|
||
# Check if storage exists and is active
|
||
if [[ -z "$storage_line" ]]; then
|
||
[[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' not found!\n\nThe storage may be unavailable or disabled." 10 60
|
||
return 236
|
||
fi
|
||
|
||
# Check storage status (column 3)
|
||
local status
|
||
status=$(awk '{print $3}' <<<"$storage_line")
|
||
if [[ "$status" == "disabled" ]]; then
|
||
[[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' is disabled!\n\nPlease enable the storage first." 10 60
|
||
return 236
|
||
fi
|
||
|
||
# Get storage type and free space (column 6)
|
||
local storage_type storage_free
|
||
storage_type=$(awk '{print $2}' <<<"$storage_line")
|
||
storage_free=$(awk '{print $6}' <<<"$storage_line")
|
||
|
||
# Some storage types (like PBS, iSCSI) don't report size info
|
||
# In these cases, skip space validation
|
||
if [[ -z "$storage_free" || "$storage_free" == "0" ]]; then
|
||
# Silent pass for storages without size info
|
||
return 0
|
||
fi
|
||
|
||
local required_kb=$((required_gb * 1024 * 1024))
|
||
local free_gb_fmt
|
||
free_gb_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$storage_free" 2>/dev/null || echo "${storage_free}KB")
|
||
|
||
if [[ "$storage_free" -lt "$required_kb" ]]; then
|
||
if [[ "$show_dialog" == "yes" ]]; then
|
||
whiptail --msgbox "⚠️ Warning: Storage '$storage' may not have enough space!\n\nStorage Type: ${storage_type}\nRequired: ${required_gb}GB\nAvailable: ${free_gb_fmt}\n\nYou can continue, but creation might fail." 14 70
|
||
fi
|
||
return 236
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 8: CONTAINER CREATION
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# create_lxc_container()
|
||
#
|
||
# - Main function for creating LXC containers
|
||
# - Handles all phases: validation, template discovery, container creation,
|
||
# network config, storage, etc.
|
||
# - Extensive error checking with detailed exit codes
|
||
# ------------------------------------------------------------------------------
|
||
create_lxc_container() {
|
||
# ------------------------------------------------------------------------------
|
||
# Optional verbose mode (debug tracing)
|
||
# ------------------------------------------------------------------------------
|
||
if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Helpers (dynamic versioning / template parsing)
|
||
# ------------------------------------------------------------------------------
|
||
pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
|
||
pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }
|
||
|
||
ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
|
||
ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
|
||
ver_lt() { dpkg --compare-versions "$1" lt "$2"; }
|
||
|
||
# Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
|
||
parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }
|
||
|
||
# Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
|
||
# Returns:
|
||
# 0 = no upgrade needed
|
||
# 1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
|
||
# 2 = user declined
|
||
# 3 = upgrade attempted but failed OR retry failed
|
||
offer_lxc_stack_upgrade_and_maybe_retry() {
|
||
local do_retry="${1:-no}" # yes|no
|
||
local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0
|
||
|
||
_pvec_i="$(pkg_ver pve-container)"
|
||
_lxcp_i="$(pkg_ver lxc-pve)"
|
||
_pvec_c="$(pkg_cand pve-container)"
|
||
_lxcp_c="$(pkg_cand lxc-pve)"
|
||
|
||
if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
|
||
ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
|
||
fi
|
||
if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
|
||
ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
|
||
fi
|
||
if [[ $need -eq 0 ]]; then
|
||
msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
|
||
return 0
|
||
fi
|
||
|
||
msg_warn "An update for the Proxmox LXC stack is available"
|
||
echo " pve-container: installed=${_pvec_i:-n/a} candidate=${_pvec_c:-n/a}"
|
||
echo " lxc-pve : installed=${_lxcp_i:-n/a} candidate=${_lxcp_c:-n/a}"
|
||
echo
|
||
read -rp "Do you want to upgrade now? [y/N] " _ans </dev/tty
|
||
case "${_ans,,}" in
|
||
y | yes)
|
||
msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
|
||
apt_update_safe
|
||
if $STD apt-get install -y --only-upgrade pve-container lxc-pve; then
|
||
msg_ok "LXC stack upgraded."
|
||
if [[ "$do_retry" == "yes" ]]; then
|
||
msg_info "Retrying container creation after upgrade"
|
||
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||
msg_ok "Container created successfully after upgrade."
|
||
return 0
|
||
else
|
||
msg_error "pct create still failed after upgrade. See $LOGFILE"
|
||
return 3
|
||
fi
|
||
fi
|
||
return 1
|
||
else
|
||
msg_error "Upgrade failed. Please check APT output."
|
||
return 3
|
||
fi
|
||
;;
|
||
*) return 2 ;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Required input variables
|
||
# ------------------------------------------------------------------------------
|
||
[[ "${CTID:-}" ]] || {
|
||
msg_error "You need to set 'CTID' variable."
|
||
exit 203
|
||
}
|
||
[[ "${PCT_OSTYPE:-}" ]] || {
|
||
msg_error "You need to set 'PCT_OSTYPE' variable."
|
||
exit 204
|
||
}
|
||
|
||
msg_debug "CTID=$CTID"
|
||
msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
|
||
msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"
|
||
|
||
# ID checks
|
||
[[ "$CTID" -ge 100 ]] || {
|
||
msg_error "ID cannot be less than 100."
|
||
exit 205
|
||
}
|
||
if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
|
||
msg_warn "Container/VM ID $CTID is already in use (detected late). Reassigning..."
|
||
CTID=$(get_valid_container_id "$((CTID + 1))")
|
||
export CTID
|
||
msg_ok "Reassigned to container ID $CTID"
|
||
fi
|
||
|
||
# Report installation start to API early - captures failures in storage/template/create
|
||
post_to_api
|
||
|
||
# Transition to 'validation' — Proxmox-internal checks (storage, template, cluster)
|
||
post_progress_to_api "validation"
|
||
|
||
# Storage capability check
|
||
check_storage_support "rootdir" || {
|
||
msg_error "No valid storage found for 'rootdir' [Container]"
|
||
exit 119
|
||
}
|
||
check_storage_support "vztmpl" || {
|
||
msg_error "No valid storage found for 'vztmpl' [Template]"
|
||
exit 120
|
||
}
|
||
|
||
# Template storage selection
|
||
if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
|
||
TEMPLATE_STORAGE="$STORAGE_RESULT"
|
||
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
|
||
msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
|
||
else
|
||
while true; do
|
||
if [[ -z "${var_template_storage:-}" ]]; then
|
||
if select_storage template; then
|
||
TEMPLATE_STORAGE="$STORAGE_RESULT"
|
||
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
|
||
msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
|
||
break
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Container storage selection
|
||
if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
|
||
CONTAINER_STORAGE="$STORAGE_RESULT"
|
||
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
|
||
msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
|
||
else
|
||
if [[ -z "${var_container_storage:-}" ]]; then
|
||
if select_storage container; then
|
||
CONTAINER_STORAGE="$STORAGE_RESULT"
|
||
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
|
||
msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
msg_info "Validating storage '$CONTAINER_STORAGE'"
|
||
STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1 || true)
|
||
|
||
if [[ -z "$STORAGE_TYPE" ]]; then
|
||
msg_error "Storage '$CONTAINER_STORAGE' not found in /etc/pve/storage.cfg"
|
||
exit 213
|
||
fi
|
||
|
||
case "$STORAGE_TYPE" in
|
||
iscsidirect)
|
||
msg_error "Storage '$CONTAINER_STORAGE' uses iSCSI-direct which does not support container rootfs."
|
||
exit 212
|
||
;;
|
||
iscsi | zfs)
|
||
msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support container rootdir content."
|
||
exit 213
|
||
;;
|
||
cephfs)
|
||
msg_error "Storage '$CONTAINER_STORAGE' uses CephFS which is not supported for LXC rootfs."
|
||
exit 219
|
||
;;
|
||
pbs)
|
||
msg_error "Storage '$CONTAINER_STORAGE' is a Proxmox Backup Server — cannot be used for containers."
|
||
exit 224
|
||
;;
|
||
linstor | rbd | nfs | cifs)
|
||
if ! pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null; then
|
||
msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) is not accessible or inactive."
|
||
exit 217
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then
|
||
msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support 'rootdir' content."
|
||
exit 213
|
||
fi
|
||
msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated"
|
||
|
||
msg_info "Validating template storage '$TEMPLATE_STORAGE'"
|
||
TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 || true)
|
||
|
||
if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then
|
||
msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'"
|
||
fi
|
||
msg_ok "Template storage '$TEMPLATE_STORAGE' validated"
|
||
|
||
# Cluster quorum (if cluster)
|
||
if [[ -f /etc/pve/corosync.conf ]]; then
|
||
msg_info "Checking cluster quorum"
|
||
if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
|
||
msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
|
||
exit 210
|
||
fi
|
||
msg_ok "Cluster is quorate"
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Template discovery & validation
|
||
# ------------------------------------------------------------------------------
|
||
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
|
||
case "$PCT_OSTYPE" in
|
||
debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
|
||
alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
|
||
*) TEMPLATE_PATTERN="" ;;
|
||
esac
|
||
|
||
msg_info "Searching for template '$TEMPLATE_SEARCH'"
|
||
|
||
# Initialize variables
|
||
ONLINE_TEMPLATE=""
|
||
ONLINE_TEMPLATES=()
|
||
|
||
# Step 1: Check local templates first (instant)
|
||
mapfile -t LOCAL_TEMPLATES < <(
|
||
pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
|
||
awk -v search="${TEMPLATE_SEARCH}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
|
||
sed 's|.*/||' | sort -t - -k 2 -V
|
||
)
|
||
|
||
# Step 2: If local template found, use it immediately (skip pveam update)
|
||
if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
|
||
TEMPLATE="${LOCAL_TEMPLATES[-1]}"
|
||
TEMPLATE_SOURCE="local"
|
||
msg_ok "Template search completed"
|
||
else
|
||
# Step 3: No local template - need to check online (this may be slow)
|
||
msg_info "No local template found, checking online catalog..."
|
||
|
||
# Update catalog with timeout to prevent long hangs
|
||
if command -v timeout &>/dev/null; then
|
||
if ! timeout 30 pveam update >/dev/null 2>&1; then
|
||
msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose."
|
||
fi
|
||
else
|
||
pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)"
|
||
fi
|
||
|
||
ONLINE_TEMPLATES=()
|
||
mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^${TEMPLATE_SEARCH}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
|
||
[[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
|
||
|
||
TEMPLATE="$ONLINE_TEMPLATE"
|
||
TEMPLATE_SOURCE="online"
|
||
msg_ok "Template search completed"
|
||
fi
|
||
|
||
# If still no template, try to find alternatives
|
||
if [[ -z "$TEMPLATE" ]]; then
|
||
msg_warn "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."
|
||
|
||
# Get all available versions for this OS type
|
||
AVAILABLE_VERSIONS=()
|
||
mapfile -t AVAILABLE_VERSIONS < <(
|
||
pveam available -section system 2>/dev/null |
|
||
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
|
||
awk -F'\t' '{print $1}' |
|
||
grep "^${PCT_OSTYPE}-" |
|
||
sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" |
|
||
sort -u -V 2>/dev/null
|
||
)
|
||
|
||
if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
|
||
echo ""
|
||
echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
|
||
for i in "${!AVAILABLE_VERSIONS[@]}"; do
|
||
echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
|
||
done
|
||
echo ""
|
||
read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice </dev/tty
|
||
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
|
||
PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
|
||
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"
|
||
|
||
ONLINE_TEMPLATES=()
|
||
mapfile -t ONLINE_TEMPLATES < <(
|
||
pveam available -section system 2>/dev/null |
|
||
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
|
||
awk '{print $2}' |
|
||
grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" |
|
||
sort -t - -k 2 -V 2>/dev/null || true
|
||
)
|
||
|
||
if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
|
||
TEMPLATE="${ONLINE_TEMPLATES[-1]}"
|
||
TEMPLATE_SOURCE="online"
|
||
else
|
||
msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
|
||
exit 225
|
||
fi
|
||
else
|
||
msg_custom "🚫" "${YW}" "Installation cancelled"
|
||
exit 0
|
||
fi
|
||
else
|
||
msg_error "No ${PCT_OSTYPE} templates available at all"
|
||
exit 225
|
||
fi
|
||
fi
|
||
|
||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
|
||
if [[ -z "$TEMPLATE_PATH" ]]; then
|
||
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
|
||
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
|
||
fi
|
||
|
||
# If we still don't have a path but have a valid template name, construct it
|
||
if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
|
||
TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
|
||
fi
|
||
|
||
[[ -n "$TEMPLATE_PATH" ]] || {
|
||
if [[ -z "$TEMPLATE" ]]; then
|
||
msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"
|
||
|
||
# Get available versions
|
||
mapfile -t AVAILABLE_VERSIONS < <(
|
||
pveam available -section system 2>/dev/null |
|
||
grep "^${PCT_OSTYPE}-" |
|
||
sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
|
||
grep -E '^[0-9]+\.[0-9]+$' |
|
||
sort -u -V 2>/dev/null || sort -u
|
||
)
|
||
|
||
if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
|
||
echo -e "\n${BL}Available versions:${CL}"
|
||
for i in "${!AVAILABLE_VERSIONS[@]}"; do
|
||
echo " [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
|
||
done
|
||
|
||
echo ""
|
||
read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice </dev/tty
|
||
|
||
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
|
||
export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
|
||
export PCT_OSVERSION="$var_version"
|
||
msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"
|
||
|
||
# Retry template search with new version
|
||
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
|
||
|
||
mapfile -t LOCAL_TEMPLATES < <(
|
||
pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
|
||
awk -v search="${TEMPLATE_SEARCH}-" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
|
||
sed 's|.*/||' | sort -t - -k 2 -V
|
||
)
|
||
mapfile -t ONLINE_TEMPLATES < <(
|
||
pveam available -section system 2>/dev/null |
|
||
grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
|
||
awk '{print $2}' |
|
||
grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" |
|
||
sort -t - -k 2 -V 2>/dev/null || true
|
||
)
|
||
ONLINE_TEMPLATE=""
|
||
[[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"
|
||
|
||
if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
|
||
TEMPLATE="${LOCAL_TEMPLATES[-1]}"
|
||
TEMPLATE_SOURCE="local"
|
||
else
|
||
TEMPLATE="$ONLINE_TEMPLATE"
|
||
TEMPLATE_SOURCE="online"
|
||
fi
|
||
|
||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
|
||
if [[ -z "$TEMPLATE_PATH" ]]; then
|
||
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
|
||
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
|
||
fi
|
||
|
||
# If we still don't have a path but have a valid template name, construct it
|
||
if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
|
||
TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
|
||
fi
|
||
|
||
[[ -n "$TEMPLATE_PATH" ]] || {
|
||
msg_error "Template still not found after version change"
|
||
exit 220
|
||
}
|
||
else
|
||
msg_custom "🚫" "${YW}" "Installation cancelled"
|
||
exit 0
|
||
fi
|
||
else
|
||
msg_error "No ${PCT_OSTYPE} templates available"
|
||
exit 220
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Validate that we found a template
|
||
if [[ -z "$TEMPLATE" ]]; then
|
||
msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
|
||
msg_custom "ℹ️" "${YW}" "Please check:"
|
||
msg_custom " •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)"
|
||
msg_custom " •" "${YW}" "Does the template exist for your OS version?"
|
||
exit 225
|
||
fi
|
||
|
||
msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
|
||
msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"
|
||
|
||
NEED_DOWNLOAD=0
|
||
if [[ ! -f "$TEMPLATE_PATH" ]]; then
|
||
msg_info "Template not present locally – will download."
|
||
NEED_DOWNLOAD=1
|
||
elif [[ ! -r "$TEMPLATE_PATH" ]]; then
|
||
msg_error "Template file exists but is not readable – check permissions."
|
||
exit 221
|
||
elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
|
||
if [[ -n "$ONLINE_TEMPLATE" ]]; then
|
||
msg_warn "Template file too small (<1MB) – re-downloading."
|
||
NEED_DOWNLOAD=1
|
||
else
|
||
msg_warn "Template looks too small, but no online version exists. Keeping local file."
|
||
fi
|
||
elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
|
||
if [[ -n "$ONLINE_TEMPLATE" ]]; then
|
||
msg_warn "Template appears corrupted – re-downloading."
|
||
NEED_DOWNLOAD=1
|
||
else
|
||
msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
|
||
fi
|
||
else
|
||
$STD msg_ok "Template $TEMPLATE is present and valid."
|
||
fi
|
||
|
||
if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
|
||
msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
|
||
if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
|
||
TEMPLATE="$ONLINE_TEMPLATE"
|
||
NEED_DOWNLOAD=1
|
||
else
|
||
msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE"
|
||
fi
|
||
fi
|
||
|
||
if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
|
||
[[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
|
||
for attempt in {1..3}; do
|
||
msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
|
||
if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1; then
|
||
msg_ok "Template download successful."
|
||
break
|
||
fi
|
||
if [[ $attempt -eq 3 ]]; then
|
||
msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
|
||
exit 222
|
||
fi
|
||
sleep $((attempt * 5))
|
||
done
|
||
fi
|
||
|
||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
|
||
msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
|
||
exit 223
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
|
||
# ------------------------------------------------------------------------------
|
||
if [[ "$PCT_OSTYPE" == "debian" ]]; then
|
||
OSVER="$(parse_template_osver "$TEMPLATE")"
|
||
if [[ -n "$OSVER" ]]; then
|
||
offer_lxc_stack_upgrade_and_maybe_retry "no" || true
|
||
fi
|
||
fi
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Create LXC Container
|
||
# ------------------------------------------------------------------------------
|
||
msg_info "Creating LXC container"
|
||
|
||
# Ensure subuid/subgid entries exist
|
||
grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
|
||
grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
|
||
|
||
# PCT_OPTIONS is now a string (exported from build_container)
|
||
# Add rootfs if not already specified
|
||
if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then
|
||
PCT_OPTIONS="$PCT_OPTIONS
|
||
-rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}"
|
||
fi
|
||
|
||
# Lock by template file (avoid concurrent template downloads/validation)
|
||
lockfile="/tmp/template.${TEMPLATE}.lock"
|
||
|
||
# Cleanup stale lock files (older than 1 hour - likely from crashed processes)
|
||
if [[ -f "$lockfile" ]]; then
|
||
local lock_age=$(($(date +%s) - $(stat -c %Y "$lockfile" 2>/dev/null || echo 0)))
|
||
if [[ $lock_age -gt 3600 ]]; then
|
||
msg_warn "Removing stale template lock file (age: ${lock_age}s)"
|
||
rm -f "$lockfile"
|
||
fi
|
||
fi
|
||
|
||
exec 9>"$lockfile" || {
|
||
msg_error "Failed to create lock file '$lockfile'."
|
||
exit 200
|
||
}
|
||
|
||
# Retry logic for template lock (another container creation may be running)
|
||
local lock_attempts=0
|
||
local max_lock_attempts=10
|
||
local lock_wait_time=30
|
||
|
||
while ! flock -w "$lock_wait_time" 9; do
|
||
lock_attempts=$((lock_attempts + 1))
|
||
if [[ $lock_attempts -ge $max_lock_attempts ]]; then
|
||
msg_error "Timeout while waiting for template lock after ${max_lock_attempts} attempts."
|
||
msg_custom "💡" "${YW}" "Another container creation may be stuck. Check running processes or remove: $lockfile"
|
||
exit 211
|
||
fi
|
||
msg_custom "⏳" "${YW}" "Another container is being created with this template. Waiting... (attempt ${lock_attempts}/${max_lock_attempts})"
|
||
done
|
||
|
||
LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
|
||
|
||
# Helper: append pct_create log to BUILD_LOG before exit so combined log has full context
|
||
_flush_pct_log() {
|
||
if [[ -s "${LOGFILE:-}" && -n "${BUILD_LOG:-}" ]]; then
|
||
{
|
||
echo ""
|
||
echo "--- pct create output (${LOGFILE}) ---"
|
||
cat "$LOGFILE"
|
||
echo "--- end pct create output ---"
|
||
} >>"$BUILD_LOG" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# Validate template before pct create (while holding lock)
|
||
if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH" 2>/dev/null || echo 0)" -lt 1000000 ]]; then
|
||
msg_info "Template file missing or too small – downloading"
|
||
rm -f "$TEMPLATE_PATH"
|
||
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 || {
|
||
msg_error "Failed to download template '$TEMPLATE' to storage '$TEMPLATE_STORAGE'"
|
||
exit 222
|
||
}
|
||
msg_ok "Template downloaded"
|
||
elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
|
||
if [[ -n "$ONLINE_TEMPLATE" ]]; then
|
||
msg_info "Template appears corrupted – re-downloading"
|
||
rm -f "$TEMPLATE_PATH"
|
||
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 || {
|
||
msg_error "Failed to re-download template '$TEMPLATE'"
|
||
exit 222
|
||
}
|
||
msg_ok "Template re-downloaded"
|
||
else
|
||
msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
|
||
fi
|
||
fi
|
||
|
||
# Release lock after template validation - pct create has its own internal locking
|
||
exec 9>&-
|
||
|
||
msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS"
|
||
msg_debug "Logfile: $LOGFILE"
|
||
|
||
# First attempt (PCT_OPTIONS is a multi-line string, use it directly)
|
||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
|
||
msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..."
|
||
|
||
# Check if CTID collision (race condition: ID claimed between validation and creation)
|
||
if grep -qiE 'already exists|already in use' "$LOGFILE"; then
|
||
local old_ctid="$CTID"
|
||
CTID=$(get_valid_container_id "$((CTID + 1))")
|
||
export CTID
|
||
msg_warn "Container ID $old_ctid was claimed by another process. Retrying with ID $CTID"
|
||
LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
|
||
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
|
||
msg_ok "Container successfully created with new ID $CTID"
|
||
else
|
||
msg_error "Container creation failed even with new ID $CTID. See $LOGFILE"
|
||
_flush_pct_log
|
||
exit 209
|
||
fi
|
||
else
|
||
# Not a CTID collision - check if template issue and retry with fresh download
|
||
if grep -qiE 'unable to open|corrupt|invalid' "$LOGFILE"; then
|
||
msg_info "Template may be corrupted – re-downloading"
|
||
rm -f "$TEMPLATE_PATH"
|
||
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1
|
||
msg_ok "Template re-downloaded"
|
||
fi
|
||
|
||
# Retry after repair
|
||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||
# Fallback to local storage if not already on local
|
||
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
|
||
msg_info "Retrying container creation with fallback to local storage"
|
||
LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
|
||
if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
|
||
msg_ok "Trying local storage fallback"
|
||
msg_info "Downloading template to local"
|
||
pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1
|
||
msg_ok "Template downloaded to local"
|
||
else
|
||
msg_ok "Trying local storage fallback"
|
||
fi
|
||
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
|
||
# Local fallback also failed - check for LXC stack version issue
|
||
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
|
||
msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
|
||
offer_lxc_stack_upgrade_and_maybe_retry "yes"
|
||
rc=$?
|
||
case $rc in
|
||
0) : ;; # success - container created, continue
|
||
2)
|
||
msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve"
|
||
_flush_pct_log
|
||
exit 231
|
||
;;
|
||
3)
|
||
msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE"
|
||
_flush_pct_log
|
||
exit 231
|
||
;;
|
||
esac
|
||
else
|
||
msg_error "Container creation failed. See $LOGFILE"
|
||
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
|
||
set -x
|
||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
|
||
set +x
|
||
fi
|
||
_flush_pct_log
|
||
exit 209
|
||
fi
|
||
else
|
||
msg_ok "Container successfully created using local fallback."
|
||
fi
|
||
else
|
||
# Already on local storage and still failed - check LXC stack version
|
||
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
|
||
msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
|
||
offer_lxc_stack_upgrade_and_maybe_retry "yes"
|
||
rc=$?
|
||
case $rc in
|
||
0) : ;; # success - container created, continue
|
||
2)
|
||
msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve"
|
||
_flush_pct_log
|
||
exit 231
|
||
;;
|
||
3)
|
||
msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE"
|
||
_flush_pct_log
|
||
exit 231
|
||
;;
|
||
esac
|
||
else
|
||
msg_error "Container creation failed. See $LOGFILE"
|
||
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
|
||
set -x
|
||
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
|
||
set +x
|
||
fi
|
||
_flush_pct_log
|
||
exit 209
|
||
fi
|
||
fi
|
||
else
|
||
msg_ok "Container successfully created after template repair."
|
||
fi
|
||
fi # close CTID collision else-branch
|
||
fi
|
||
|
||
# Verify container exists (allow up to 10s for pmxcfs sync in clusters)
|
||
local _pct_visible=false
|
||
for _pct_check in {1..10}; do
|
||
if pct list | awk '{print $1}' | grep -qx "$CTID"; then
|
||
_pct_visible=true
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
if [[ "$_pct_visible" != true ]]; then
|
||
msg_error "Container ID $CTID not listed in 'pct list' after 10s. See $LOGFILE"
|
||
msg_custom "🔧" "${YW}" "This can happen in clusters with pmxcfs sync delays."
|
||
_flush_pct_log
|
||
exit 215
|
||
fi
|
||
|
||
# Verify config rootfs
|
||
grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
|
||
msg_error "RootFS entry missing in container config. See $LOGFILE"
|
||
_flush_pct_log
|
||
exit 216
|
||
}
|
||
|
||
msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
|
||
|
||
# Append pct create log to BUILD_LOG for combined log visibility
|
||
if [[ -s "$LOGFILE" && -n "${BUILD_LOG:-}" ]]; then
|
||
{
|
||
echo ""
|
||
echo "--- pct create output ---"
|
||
cat "$LOGFILE"
|
||
echo "--- end pct create output ---"
|
||
} >>"$BUILD_LOG" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 9: POST-INSTALLATION & FINALIZATION
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# description()
|
||
#
|
||
# - Sets container description with formatted HTML content
|
||
# - Includes:
|
||
# * Community-Scripts logo
|
||
# * Application name
|
||
# * Links to GitHub, Discussions, Issues
|
||
# * Ko-fi donation badge
|
||
# - Restarts ping-instances.service if present (monitoring)
|
||
# - Posts final "done" status to API telemetry
|
||
# ------------------------------------------------------------------------------
|
||
description() {
|
||
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
|
||
local script_slug script_url donate_url
|
||
|
||
script_slug="${SCRIPT_SLUG:-${NSAPP}}"
|
||
script_slug="$(echo "$script_slug" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')"
|
||
script_url="https://community-scripts.org/scripts/${script_slug}"
|
||
donate_url="https://community-scripts.org/donate"
|
||
|
||
# Generate LXC Description
|
||
DESCRIPTION=$(
|
||
cat <<EOF
|
||
<div align='center'>
|
||
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
|
||
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
|
||
</a>
|
||
|
||
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
|
||
|
||
<p style='margin: 16px 0;'>
|
||
<a href='${donate_url}' target='_blank' rel='noopener noreferrer'>
|
||
<img src='https://img.shields.io/badge/❤️-Sponsoring%20%26%20Donations-FF5E5B' alt='Sponsoring and donations' />
|
||
</a>
|
||
</p>
|
||
|
||
<p style='margin: 12px 0;'>
|
||
<a href='${script_url}' target='_blank' rel='noopener noreferrer'>
|
||
<img src='https://img.shields.io/badge/📦-Open%20Script%20Page-00617f' alt='Open script page' />
|
||
</a>
|
||
</p>
|
||
|
||
<span style='margin: 0 10px;'>
|
||
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
|
||
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
|
||
</span>
|
||
<span style='margin: 0 10px;'>
|
||
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
|
||
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
|
||
</span>
|
||
<span style='margin: 0 10px;'>
|
||
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
|
||
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
|
||
</span>
|
||
</div>
|
||
EOF
|
||
)
|
||
pct set "$CTID" -description "$DESCRIPTION"
|
||
|
||
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
|
||
systemctl start ping-instances.service
|
||
fi
|
||
|
||
INSTALL_COMPLETE=true
|
||
post_update_to_api "done" "none"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 10: ERROR HANDLING & EXIT TRAPS
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ensure_log_on_host()
|
||
#
|
||
# - Ensures INSTALL_LOG points to a readable file on the host
|
||
# - If INSTALL_LOG points to a container path (e.g. /root/.install-*),
|
||
# tries to pull it from the container and create a combined log
|
||
# - This allows get_error_text() to find actual error output for telemetry
|
||
# - Uses timeout on pct pull to prevent hangs on dead/unresponsive containers
|
||
# ------------------------------------------------------------------------------
|
||
ensure_log_on_host() {
|
||
# Already readable on host? Nothing to do.
|
||
[[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]] && return 0
|
||
|
||
# Try pulling from container and creating combined log
|
||
if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]] && command -v pct &>/dev/null; then
|
||
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
|
||
if [[ ! -s "$combined_log" ]]; then
|
||
# Create combined log
|
||
{
|
||
echo "================================================================================"
|
||
echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
|
||
echo "Container ID: ${CTID}"
|
||
echo "Session ID: ${SESSION_ID}"
|
||
echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo "================================================================================"
|
||
echo ""
|
||
} >"$combined_log" 2>/dev/null || return 0
|
||
# Append BUILD_LOG if it exists
|
||
if [[ -f "${BUILD_LOG:-}" ]]; then
|
||
{
|
||
echo "================================================================================"
|
||
echo "PHASE 1: CONTAINER CREATION (Host)"
|
||
echo "================================================================================"
|
||
cat "${BUILD_LOG}"
|
||
echo ""
|
||
} >>"$combined_log"
|
||
fi
|
||
# Pull INSTALL_LOG from container (with timeout to prevent hangs on dead containers)
|
||
local temp_log="/tmp/.install-temp-${SESSION_ID}.log"
|
||
if timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then
|
||
{
|
||
echo "================================================================================"
|
||
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
|
||
echo "================================================================================"
|
||
cat "$temp_log"
|
||
echo ""
|
||
} >>"$combined_log"
|
||
rm -f "$temp_log"
|
||
fi
|
||
fi
|
||
if [[ -s "$combined_log" ]]; then
|
||
INSTALL_LOG="$combined_log"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# TRAP MANAGEMENT
|
||
# ==============================================================================
|
||
# All traps (ERR, EXIT, INT, TERM, HUP) are set by catch_errors() in
|
||
# error_handler.func — called at the top of this file after sourcing.
|
||
#
|
||
# Do NOT set duplicate traps here. The handlers in error_handler.func
|
||
# (on_exit, on_interrupt, on_terminate, on_hangup, error_handler) already:
|
||
# - Send telemetry via post_update_to_api / _send_abort_telemetry
|
||
# - Stop orphaned containers via _stop_container_if_installing
|
||
# - Collect logs via ensure_log_on_host
|
||
# - Clean up lock files and spinner processes
|
||
#
|
||
# Previously, inline traps here overwrote catch_errors() traps, causing:
|
||
# - error_handler() never fired (no error output, no cleanup dialog)
|
||
# - on_hangup() never fired (SSH disconnect → stuck records)
|
||
# - Duplicated logic in two places (hard to debug)
|
||
# ==============================================================================
|