diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..efba309 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +# EditorConfig - https://editorconfig.org +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.sh] +indent_style = space +indent_size = 4 + +[*.py] +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{json,json.template}] +indent_style = tab + +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..42c7c4f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,13 @@ +# Default owner for all files +* @coelacant1 + +# CI and repository checks +.github/ @coelacant1 +.check/ @coelacant1 + +# Utility framework +Utilities/ @coelacant1 + +# GUI entry point +GUI.sh @coelacant1 +CCPVE.sh @coelacant1 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..57979b8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + labels: + - "dependencies" + commit-message: + prefix: "ci" diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7652ed7..8e7663c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version: '3.12' - name: Install shellcheck run: | diff --git a/.gitignore b/.gitignore index 34dc0f5..2995863 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,31 @@ -#Custom +# IDE and editor .vscode/ *.code-workspace +.idea/ +*.swp +*.swo +*~ + +# OS artifacts +.DS_Store +Thumbs.db + +# Environment and secrets +.env +.env.local nodes.json TestConnectionInfo.json -.d/ + +# Temporary and log files +*.log +*.tmp + +# NFS and stale file handles .nfs* -*/__pycache__/ -*/__pycache__/* + +# Python caches __pycache__/ -__pycache__/* +*/__pycache__/ # Downloaded Proxmox documentation (keep scripts, ignore generated content) .docs/*.html diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..86335b6 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,10 @@ +# ShellCheck configuration +# https://www.shellcheck.net/wiki/ + +# Allow sourcing from paths that can't be resolved at analysis time +# (UTILITYPATH is set at runtime) +external-sources=true + +# Disabled checks: +# SC1091 - Not following sourced files (resolved via external-sources) +disable=SC1091 diff --git a/CHANGELOG.md b/CHANGELOG.md index e1310e3..470a966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.1] - 2026-04-29 + +Online ext4 partition expansion fix + +### Fixed +- **ExpandEXT4Partition** - Resize now succeeds on mounted filesystems + - Previously parted's heredoc fed `Yes` to a `Fix/Ignore?` GPT-recovery prompt, + producing `parted: invalid token: Yes` and silently leaving the partition + unchanged while the script reported success + - Replaced parted heredoc with `sgdisk` delete + recreate at the same start + sector, preserving partition GUID, type code and name (mountpoint stays valid) + - Removed the `umount` step entirely; uses `partx -u` to refresh the kernel + view online and `resize2fs` to grow ext4 while mounted + - Added a `blockdev --getsize64` verification step that fails loudly if the + kernel does not see the new partition size before `resize2fs` runs + - Dropped the `parted` install dependency (no longer used) + ## [2.2.0] - 2026-03-02 Security hardening, performance optimizations, and GUI improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 474a374..5b6e0be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,15 +16,25 @@ This repository contains Bash scripts (`.sh` files) that help automate and manag - Fork the repository to your own GitHub account. - Clone your fork locally. -2. **Make Your Changes** +2. **Create a Branch** + - Create a feature branch from `main` for your changes. + - Use a descriptive name (e.g., `fix/bulk-migrate-timeout`, `feature/add-serial-port`). + +3. **Make Your Changes** - Follow the coding guidelines below. - **Use the [Script Compliance Checklist](Utilities/_ScriptComplianceChecklist.md)** to ensure your script follows all conventions. - Test your changes thoroughly in a development/test environment. -3. **Submit a Pull Request** +4. **Submit a Pull Request** - Open a pull request (PR) against the repository’s `main` branch. - Provide a clear title and description, referencing any related issues. + - All PRs are reviewed by maintainers before merging. + +### Branching Model +- **`main`** — Stable release branch. All PRs target this branch. +- **`testing`** — Active development and integration testing before release. +- **Feature branches** — Created from `main` for individual changes. --- ## 3. Shell Script Style Guide diff --git a/Host/Storage/ExpandEXT4Partition.sh b/Host/Storage/ExpandEXT4Partition.sh index 0d8faa2..3f0b15e 100755 --- a/Host/Storage/ExpandEXT4Partition.sh +++ b/Host/Storage/ExpandEXT4Partition.sh @@ -3,7 +3,9 @@ # ExpandEXT4Partition.sh # # Expands a GPT partition and ext4 filesystem to use available disk space. -# Uses parted for safe in-place partition resizing without destroying metadata. +# Uses sgdisk to recreate the partition non-interactively (preserving start +# sector, UUID, type and name) so the operation works on mounted filesystems +# without unmounting. # # Usage: # ExpandEXT4Partition.sh @@ -43,7 +45,6 @@ main() { __check_proxmox__ __install_or_prompt__ "gdisk" - __install_or_prompt__ "parted" __install_or_prompt__ "util-linux" __install_or_prompt__ "e2fsprogs" @@ -63,10 +64,13 @@ main() { exit 0 fi - # Fix GPT if needed + # Fix GPT if needed (relocate secondary header to end of disk). + # When partitions on the device are mounted, partprobe cannot re-read the + # in-kernel partition table; refresh per-partition state with partx -u so + # parted sees a consistent view on the next invocation. __info__ "Fixing GPT table if needed" sgdisk -e "$DEVICE" 2>&1 || true - partprobe "$DEVICE" 2>&1 || true + partprobe "$DEVICE" 2>&1 || partx -u "$DEVICE" 2>&1 || true sleep 2 # Verify exactly one partition exists @@ -78,44 +82,110 @@ main() { exit 1 fi - # Unmount if mounted + # Detect mount state - prefer online resize so we don't have to unmount local mountpoint - mountpoint=$(lsblk -no MOUNTPOINT "$partition" 2>/dev/null || true) + mountpoint=$(lsblk -no MOUNTPOINT "$partition" 2>/dev/null | head -n1 || true) + local was_mounted=0 + local online_resize=0 if [[ -n "$mountpoint" ]]; then - __info__ "Unmounting ${partition} from ${mountpoint}" - if umount "$partition" 2>&1; then - __ok__ "Unmounted successfully" - else - __err__ "Could not unmount ${partition}" - exit 1 - fi + was_mounted=1 + __info__ "${partition} is mounted at ${mountpoint}" + __info__ "Attempting online expansion (no unmount required)" + online_resize=1 fi - # Use parted to resize partition in-place (safer than recreating table) - __info__ "Resizing partition to use all available space" - if parted "$DEVICE" ---pretend-input-tty <&1 -resizepart 1 100% -Yes -EOF - then - __ok__ "Partition resized successfully" - else - __err__ "Failed to resize partition" + # Capture pre-resize partition size so we can verify the resize actually + # changed something + local size_before + size_before=$(blockdev --getsize64 "$partition" 2>/dev/null || echo 0) + + # Resize partition non-interactively using sgdisk. parted's interactive + # heredoc approach is brittle: the "Fix/Ignore?" GPT-recovery prompt and + # the "partition in use" Yes/No prompt both consume our input lines and + # cause "invalid token" errors. sgdisk has no prompts and works fine on + # mounted devices because it only writes the on-disk GPT - the kernel + # picks up the change later via partx -u. + # + # Read the current partition's first sector, type code, and + # unique GUID, then delete partition 1 and recreate it at the same start + # spanning to the end of the disk (0 = end). This is what + # `parted resizepart 1 100%` does, just without the prompts. + __info__ "Resizing partition to use all available space (sgdisk)" + + local part_info first_sector part_type part_uuid part_name + part_info=$(sgdisk -i 1 "$DEVICE" 2>&1) || { + __err__ "Failed to read partition 1 info" + echo "$part_info" + exit 1 + } + + first_sector=$(echo "$part_info" | awk -F': ' '/First sector/ {print $2}' | awk '{print $1}') + part_type=$(echo "$part_info" | awk -F': ' '/Partition GUID code/ {print $2}' | awk '{print $1}') + part_uuid=$(echo "$part_info" | awk -F': ' '/Partition unique GUID/ {print $2}' | awk '{print $1}') + part_name=$(echo "$part_info" | awk -F"'" '/Partition name/ {print $2}') + + if [[ -z "$first_sector" || -z "$part_type" || -z "$part_uuid" ]]; then + __err__ "Could not parse partition metadata from sgdisk:" + echo "$part_info" exit 1 fi - partprobe "$DEVICE" 2>&1 || true + __info__ "Recreating partition 1 (start=${first_sector}, type=${part_type}, uuid=${part_uuid})" + + local sgdisk_args=( + -d 1 + -n "1:${first_sector}:0" + -t "1:${part_type}" + -u "1:${part_uuid}" + ) + if [[ -n "$part_name" ]]; then + sgdisk_args+=( -c "1:${part_name}" ) + fi + + local sgdisk_out + if ! sgdisk_out=$(sgdisk "${sgdisk_args[@]}" "$DEVICE" 2>&1); then + __err__ "sgdisk failed to recreate partition:" + echo "$sgdisk_out" + exit 1 + fi + __ok__ "Partition table updated on disk" + + # Inform the kernel of the new partition size. partprobe will fail to + # re-read the table while a partition is mounted, so fall back to + # partx --update which can update a single partition online + if [[ "$was_mounted" -eq 1 ]]; then + __info__ "Updating kernel partition table online (partx)" + partx -u "$partition" 2>&1 || partx -u "$DEVICE" 2>&1 || true + else + partprobe "$DEVICE" 2>&1 || true + fi sleep 2 - # Run e2fsck - __info__ "Checking filesystem" - if e2fsck -f -y "$partition" 2>&1; then - __ok__ "Filesystem check completed" + # Verify the kernel sees the new (larger) partition size. + local size_after + size_after=$(blockdev --getsize64 "$partition" 2>/dev/null || echo 0) + if [[ "$size_after" -le "$size_before" ]]; then + __err__ "Partition size did not increase (before=${size_before} after=${size_after})" + __err__ "sgdisk output:" + echo "$sgdisk_out" + exit 1 + fi + __ok__ "Kernel sees new partition size: ${size_after} bytes (was ${size_before})" + + # e2fsck cannot run on a mounted filesystem; skip when online. + if [[ "$online_resize" -eq 0 ]]; then + __info__ "Checking filesystem" + if e2fsck -f -y "$partition" 2>&1; then + __ok__ "Filesystem check completed" + else + __warn__ "Filesystem check reported issues" + fi else - __warn__ "Filesystem check reported issues" + __info__ "Skipping e2fsck (filesystem is mounted)" fi - # Resize filesystem + # Resize the ext4 filesystem. resize2fs supports online growth on + # mounted ext4 filesystems. __info__ "Resizing ext4 filesystem" if resize2fs "$partition" 2>&1; then __ok__ "Filesystem resized" @@ -124,17 +194,6 @@ EOF exit 1 fi - # Remount if it was mounted - if [[ -n "$mountpoint" ]]; then - __info__ "Remounting at $mountpoint" - mkdir -p "$mountpoint" 2>/dev/null || true - if mount "$partition" "$mountpoint" 2>&1; then - __ok__ "Remounted successfully" - else - __warn__ "Could not remount partition" - fi - fi - echo __ok__ "Partition expansion completed successfully!" __info__ "Verify with: lsblk or df -h" @@ -147,21 +206,30 @@ main "$@" ############################################################################### # Script notes: ############################################################################### -# Last checked: 2025-11-20 +# Last checked: 2026-04-29 # # Changes: +# - 2026-04-29: Switched from parted heredoc to sgdisk for non-interactive +# resize - parted's Fix/Ignore + in-use Yes/No prompts could not be +# answered reliably and left the partition unchanged +# - 2026-04-29: Online expansion - no unmount required for mounted ext4 +# - 2026-04-29: Verify kernel sees new partition size before resize2fs # - 2025-11-20: Updated to use utility functions -# - 2025-11-20: Pending validation # - 2025-11-20: Updated to use ArgumentParser.sh # - 2025-11-20: Added proper nvme/mmcblk partition naming support # - 2025-11-20: Validated against CONTRIBUTING.md -# - Non-interactive operation using sgdisk and sfdisk (note: now uses parted) # # Fixes: +# - 2026-04-29: FIXED: parted heredoc sent "Yes" to a Fix/Ignore prompt, +# causing "invalid token" errors and leaving the partition at its original +# size while the script reported success. Replaced with sgdisk delete + +# recreate at the same start sector preserving GUID/type/name, and added a +# blockdev size verification step before calling resize2fs. +# - 2026-04-29: FIXED: umount failed when the partition was in use +# (e.g. /mnt/pve/storage). Online resize is now the default path. # - 2025-11-20: Fixed partition count logic (was counting device + partitions) -# - 2025-11-20: Fixed to use parted resizepart instead of recreating partition table # # Known issues: -# - Pending validation +# - # diff --git a/ProxmoxScripts.code-workspace b/ProxmoxScripts.code-workspace index 876a149..de6f6e6 100644 --- a/ProxmoxScripts.code-workspace +++ b/ProxmoxScripts.code-workspace @@ -4,5 +4,22 @@ "path": "." } ], - "settings": {} + "settings": { + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "[shellscript]": { + "editor.tabSize": 4, + "editor.insertSpaces": true + }, + "shellcheck.enable": true, + "shellcheck.run": "onType" + }, + "extensions": { + "recommendations": [ + "timonwong.shellcheck", + "foxundermoon.shell-format", + "mads-hartmann.bash-ide-vscode" + ] + } } \ No newline at end of file