53 Commits

Author SHA1 Message Date
Timur Kumakbayev 6c4411ede9 Merge pull request #95 from brngates98/issue94-add-lxc
feat: advertise rootdir for LXC root volumes (plugindata + docs)
2026-05-04 02:18:16 +05:00
Brian Gates 45bb0e24eb Update README to include LXC support details
Added support for LXC container root volumes in the plugin documentation.
2026-05-03 17:12:36 -04:00
Brian Gates 2c222ef704 Update plugindata to include rootdir in content 2026-05-03 17:11:57 -04:00
Timur Kumakbayev 40eb92df32 Merge pull request #93 from brngates98/issue84
Refactor pod capacity retrieval and space calculations
2026-05-04 01:58:30 +05:00
Brian Gates f7d06166fc Run Perltidy and fix linting issues 2026-05-03 16:48:34 -04:00
Brian Gates ed358b13c5 Fix purestorage pod status totals, usage scope, and space metrics 2026-05-03 16:27:05 -04:00
Brian Gates 397e2f5b64 Refactor pod capacity retrieval and space calculations 2026-05-03 15:28:50 -04:00
Timur Kumakbayev d6b0035d5e Merge pull request #90 from tommymcguiver/api-version-13-updates
API Version 13
2026-02-27 10:59:13 +05:00
Ken Miles e0e4009426 Merge remote-tracking branch 'origin/main' into api-version-13-updates 2026-02-26 09:49:04 +10:00
Timur Kumakbayev c1ff0acf15 Merge pull request #87 from tommymcguiver/syslogging-improvments-2
Improve syslogging implementation and code
2026-02-25 15:13:00 +05:00
Ken Miles 63b2c46104 API Version 13
Modify function signatures, no functionality changes
2026-02-25 13:50:02 +10:00
Ken Miles 0ace4fe87d Improve syslogging implementation and code
Convert direct print statements to use the logger for consistent info message handling.

Also convert warn and die calls to log with the common logger for unified logging.

Removes repetition, enables logging to syslog which allows each message to be timestamped, and can more easily compare
with other systemctl logs like multipathd and iscsid daemons and proxmox services to make it easier to identify race
conditions.
2026-02-24 15:52:59 +10:00
Timur Kumakbayev c666235082 Build script and documentation updates (#83) 2026-01-18 05:05:25 +05:00
dependabot[bot] 1c719ba7f8 Bump tj-actions/changed-files from 44 to 47 in /.github/workflows (#82)
* Bump tj-actions/changed-files from 44 to 47 in /.github/workflows

Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 44 to 47.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v44...v47)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: '47'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: timansky <timansky@kolesa.team>
2026-01-18 02:48:06 +05:00
Timur Kumakbayev f9f8eed94b Active Cluster Support, Pod Quota Fixes,Token Cache and CI Improvements (#81)
feat: add Active Cluster support and improve plugin stability

## Major Features

### Active Cluster Support (Experimental) (#42)
- Add support for multiple PureStorage arrays in Active Cluster configuration
- Automatic volume connection on all arrays for high availability
- Configure via comma-separated addresses and tokens

### Pod Quota Limit Support (#69)
- Add proper handling of pod quota_limit parameter
- Use quota_limit when set, fall back to array capacity when unlimited
- Fix capacity reporting for pods with quotas

### Session Token Caching
- Implement session token caching in /etc/pve/priv/purestorage/
- Automatic token refresh at 80% of TTL to prevent expiration
- In-memory and file-based caching with jitter to prevent thundering herd

### Debug Logging
- Add configurable debug logging with 4 levels (0-3)
- Support both config parameter and PURESTORAGE_DEBUG environment variable

## Bug Fixes
- Fix volume deletion when connected to multiple hosts
- Query all connections before destroy
- Disconnect from all hosts on all arrays
- Prevents "Cannot destroy volume because it is currently connected" error

## CI/CD Improvements
- Add workflow_dispatch trigger to checks workflow
- Add option to check all files or only changed files
- Add comprehensive markdown linting with markdownlint-cli2

## Testing
- Add token caching tests (tests/token_cache_test.pl)
- Test token validation, expiration, and race conditions
- Test cleanup of expired cache files

## Refactoring (#72)
- Refactor volume removal logic to handle multiple host connections
- Improve device cleanup sequence (LVM, partitions, multipath)
- Extract connection querying logic before volume destruction
- Refactor CI/CD workflows for better maintainability and flexibility
- Improve error handling and logging throughout the plugin
- Enhance Active Cluster support with proper multi-array operations

## Code Formatting
- Format all Perl files with perltidy using .perltidyrc configuration
- Ensure consistent code style across the codebase
- Fix formatting issues in PureStoragePlugin.pm and test files

## Documentation
- Update README with new functionality
2026-01-17 17:20:28 +05:00
Timur Kumakbayev c693d189df Merge pull request #76 from plieven/fix/bump_apiver
fix: adjust api version to 12, return only max apiver from PVE
2025-10-20 15:18:53 +05:00
Peter Lieven a488acbe59 fix(apiver): return lowest apiver we support in fallback case
Signed-off-by: Peter Lieven <pl@dlhnet.de>
2025-09-22 21:23:32 +02:00
Timur Kumakbayev f9a458efae Merge pull request #65 from PwrBank/patch-2
Update README.md
2025-09-10 08:28:24 +05:00
Timur Kumakbayev 66fa249d91 Merge pull request #71 from PwrBank/patch-3
Update README.md
2025-09-10 08:26:50 +05:00
Peter Lieven b03b80a7b4 fix: adjust api version to 12, return only max apiver from PVE
Signed-off-by: Peter Lieven <pl@dlhnet.de>
2025-08-29 15:20:43 +02:00
PwrBank 14096218b9 Update README.md
Added a features section to the README, so users are able to easily identify the benefits of the plugin.

More features listed would be helpful
2025-06-24 06:22:25 -05:00
Timur Kumakbayev 422063e49a Merge pull request #68 from cbka/main
Update README.md
2025-05-26 21:54:34 +05:00
cbka 71c7074675 Update README.md
add some information about hostconfig and pvesm setup
2025-05-26 10:08:41 +02:00
PwrBank 098ea4dc4f Update README.md
Updated the cd command to reflect the folder create by git
Removed sudo from the commands
2025-03-28 13:12:01 -05:00
Timur Kumakbayev 0aebefe4d3 Merge pull request #62 from plieven/fix/snap_mount
fix: error out if snapname is set in filesystem_path
2025-03-26 12:06:35 +05:00
Peter Lieven e0fcb81e69 fix: error out if snapname is set in filesystem_path
Pure currently does not support direct mounting of snapshots.
As we currently silently ignore the snapname field at least error
out if we receive a non empty snapname. Its unclear if this is
still used as vzdump nowadays does live backups.

Signed-off-by: Peter Lieven <pl@dlhnet.de>
2025-03-19 17:16:34 +01:00
Timur Kumakbayev b4c81dd5a2 Merge pull request #55 from amulet1/cleanup3
Cleanup
2025-02-17 15:17:37 +05:00
amulet1 08955ca5e1 Use file_read_firstline() in get_device_size() 2025-02-16 13:56:02 -05:00
amulet1 6d70cb8c4b Drop $cache usage as there are no caller using it and the same storeid multiple times
* Stop using $cache in list_images()
 * Stop using $cache in status()
2025-02-15 14:05:22 -05:00
amulet1 aac760b3cb Use /sys/block/*/size to determine device size 2025-02-15 13:30:12 -05:00
Timur Kumakbayev aeb3547d5e Merge pull request #54 from amulet1/scsi_scan
Redesign to use same scanning functions for both fc and iscsi protocols
2025-02-14 12:54:40 +05:00
amulet1 87b579b65a Rescan slaves on volume resize instead of searching for wwn across all SCSI devices
Eliminate scsi_rescan_device()

Add get_device_path_wwn()

Add device_op() to serve as helper for device operations

In scsi_scan_new()
 * Use device_op()

In purestorage_get_device_size()
 * Rename to get_device_size()
 * Drop $class

In remove_block_device()
 * Rename it to block_device_action()
 * Use common code to remove or rescan devices
 * Use device_op() helper to perform a device action

Add block_device_slaves() returning resolved device path and its slaves

In purestorage_get_wwn()
 * Use get_device_path_wwn() to simplify the code

Redesign purestorage_resize_volume()
 * Avoid extra API call by using volume serial returned by volume resize action
 * Use get_device_path_wwn()
 * Remove unneeded protocol checks
 * Support [unlikely] case when resized volume is not mapped/connected
 * Use block_device_slaves() and block_device_action() to simplify the code

Redesign unmap_volume()
 * Use block_device_slaves() and block_device_action() to simplify the code
2025-02-13 19:10:24 -05:00
amulet1 b016137857 General changes to use scsi_*() functions for iSCSI mode
In scsi_scan_new()
 * Pass protocol as parameter
 * Adjust code to scan hosts for specified protocol
 * Count number of scanned hosts, report it in debug mode

In scsi_rescan_new()
 * Search for wwid in /sys/class/scsi_disk
 * Use stricter search pattern
 * Count number of found matches, report it in debug mode

Rename purestorage_unmap_disk() to remove_block_device()

In purestorage_resize_volume()
 * Report that volume was resized after its new size is confirmed
 * Use scsi_rescan_device() for iscsi
 * Change "Info :: New size detected ..." to debug output

In map_volume()
 * Use scsi_scan_new() for iscsi

In unmap_volume()
 * Remove redundant block device check
 * Change info messages to debug output
2025-02-13 11:04:10 -05:00
Timur Kumakbayev c029d3b90a Merge pull request #53 from amulet1/cleanup2
Minor changes and logging improvements
2025-02-12 21:31:42 +05:00
amulet1 f1bf737d39 Minor changes and logging improvements
Include leading "3" into $purestorage_wwn_prefix

In purestorage_get_wwn()
 * modify code to use updated $purestorage_wwn_prefix value
 * add warning if volume is not found

In exec_command()
 * die on error by default, update affected callers
 * allow to invert success/failure with warning suppression (e.g. to support "fuser")
 * update executables to full path inside of the function
 * update affected callers

In wait_for()
 * die on timeout instead of returning 0, update affected callers

In purestorage_unmap_disk()
 * drop $class
 * check that disk is a block device

In unmap_volume()
 * improve error checking and logging
 * add support for "fuser" check (but leave it commented out)

In scsi_rescan_device()
 * remove 'multipath -r' call (there is a 'multipathd resize map' call)
2025-02-12 11:01:41 -05:00
Timur Kumakbayev 14d93fab80 Merge pull request #48 from NojuHD/fc-support
Add support for SCSI (Fibre Channel)
2025-02-10 09:00:52 +05:00
NojuHD 7c77b06585 Merge branch 'main' into fc-support 2025-02-09 06:07:48 +01:00
Timur Kumakbayev 58265255f1 Merge pull request #51 from amulet1/cleanup
Multipathing cleanup and improvements
2025-02-09 01:21:44 +05:00
amulet1 d93f562bdc Multipathing cleanup and improvements
* use multipathd for certain functions
 * suppress output of exec_command() by default if $DEBUG value is less than 3
 * introduce multipath_check() function
 * eliminate purestorage_cleanup_diskmap()

Introduce wait_for() function
 * to be used instead of waiting loops
 * reduce default timeout and sleep delay
 * do not log "Waiting for..." unless it actually waited
 * show total time spent while waiting [not counting actual wait checks]

In purestorage_resize_volume()
 * replace "multipath -r" call (unneeded reload of *all* multipath maps) with "multipathd resize map" call
 * use wait_for() instead of waiting loop

In map_volume()
 * remove unneeded "multipath -a" call
 * use wait_for() instead of waiting loop
 * call "multipathd add map" unless multipath_check() succeeds

In unmap_volume()
 * use multipath_check()
 * remove unneeded "multipath -w" call
 * replace "multipath -f" with "multipathd remove map" call, die on error
2025-02-08 12:01:25 -05:00
Timur Kumakbayev a32f42999f Merge pull request #50 from amulet1/cluster
ActiveCluster support
2025-02-08 14:55:52 +05:00
NojuHD 32ed77d155 Set protocol default via "$default_protocol" 2025-02-08 00:06:34 -05:00
NojuHD 44e53e7e20 Fix typos & change name scheme 2025-02-07 21:41:14 -05:00
NojuHD e6abf236a0 Correct formatting (perltidy) 2025-02-07 12:29:05 -05:00
NojuHD 7bb26c2a7b Remove multipathd (use existing multipath) 2025-02-07 13:57:24 +01:00
Timur Kumakbayev 7b5b78ad55 Merge pull request #45 from NojuHD/main
Update README.md
2025-02-07 08:22:09 +05:00
amulet1 576a4d4181 Initial ActiveCluster support (#42)
* ActiveCluster is enabled by providing comma-separated cluster arrays parameters in "address" and "token"
 * retry non-API errors on a second array
 * connect/disconnect volumes to host on both arrays
 * related code refactoring and improvements
2025-02-06 14:56:51 -05:00
NojuHD 4f3f2d081d Added support for SCSI (Fibre Channel)
- Added subroutines
    - added sub::scsi_scan_new
    - added sub::scsi_rescan_device
- Added protocol selection
2025-02-05 19:40:18 -05:00
NojuHD 3cb2f31699 Update README.md
1. changing git clone to https so no auth is needed.
2. removing doubled "token" in pvesm template.
2025-02-04 18:36:48 +01:00
Timur Kumakbayev e7f429da28 Merge pull request #21 from amulet1/veeam
Support Veeam snapshots
2025-01-30 09:32:42 +05:00
amulet1 ac971e0425 Added support for Veeam snapshots 2025-01-29 16:28:57 -05:00
Timur Kumakbayev 06410fb99a Merge pull request #40 from amulet1/expired-session
Address API session token expirations
2025-01-30 00:59:32 +05:00
amulet1 c156355589 Redesigned purestorage_api_request() to handle session expirations 2025-01-29 14:02:50 -05:00
Timur Kumakbayev c9d4c07cde Merge pull request #38 from kolesa-team/issue-37
return path in map_volume (#37)
2025-01-29 20:00:59 +05:00
15 changed files with 2968 additions and 417 deletions
+112
View File
@@ -0,0 +1,112 @@
name: Checks
run-name: Checks on ${{ github.ref_name }} by ${{ github.actor }}
on:
pull_request:
types:
- opened
- synchronize
- reopened
workflow_dispatch:
inputs:
check_all_files:
description: 'Check all files (not just changed)'
required: false
type: boolean
default: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check-changes:
name: Check changed files
runs-on: ubuntu-22.04
outputs:
check-all: ${{ steps.set-check-all.outputs.check-all }}
perl-changed: ${{ steps.changed-files-perl.outputs.any_changed }}
markdown-changed: ${{ steps.changed-files-markdown.outputs.any_changed }}
test-changed: ${{ steps.changed-files-test.outputs.any_changed }}
perl-files: ${{ steps.set-perl-files.outputs.files }}
markdown-files: ${{ steps.set-markdown-files.outputs.files }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine if we should check all files
id: set-check-all
run: |
# Check all files only for manual workflow dispatch with check_all_files=true
# For PRs, check only changed files
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.check_all_files }}" == "true" ]; then
echo "check-all=true" >> $GITHUB_OUTPUT
else
echo "check-all=false" >> $GITHUB_OUTPUT
fi
- name: Check changed Perl files
id: changed-files-perl
uses: tj-actions/changed-files@v47
with:
files: |
**/*.pm
**/*.pl
- name: Check changed Markdown files
id: changed-files-markdown
uses: tj-actions/changed-files@v47
with:
files: |
**/*.md
- name: Check changed test files
id: changed-files-test
uses: tj-actions/changed-files@v47
with:
files: |
tests/**/*.t
tests/**/*.pl
- name: Set Perl files to check
id: set-perl-files
run: |
if [ "${{ steps.set-check-all.outputs.check-all }}" == "true" ]; then
files=$(find . -type f \( -name "*.pm" -o -name "*.pl" \) ! -path "*/node_modules/*" ! -path "*/.git/*" | tr '\n' ' ')
echo "files=$files" >> $GITHUB_OUTPUT
else
echo "files=${{ steps.changed-files-perl.outputs.all_changed_files }}" >> $GITHUB_OUTPUT
fi
- name: Set Markdown files to check
id: set-markdown-files
run: |
if [ "${{ steps.set-check-all.outputs.check-all }}" == "true" ]; then
files=$(find . -type f -name "*.md" ! -path "*/node_modules/*" ! -path "*/.git/*" | tr '\n' ' ')
echo "files=$files" >> $GITHUB_OUTPUT
else
echo "files=${{ steps.changed-files-markdown.outputs.all_changed_files }}" >> $GITHUB_OUTPUT
fi
lint:
name: Lint
needs: check-changes
if: |
(needs.check-changes.outputs.check-all == 'true') ||
(needs.check-changes.outputs.perl-changed == 'true' || needs.check-changes.outputs.markdown-changed == 'true')
uses: ./.github/workflows/lint.yml
with:
enable-perl-lint: ${{ needs.check-changes.outputs.check-all == 'true' || needs.check-changes.outputs.perl-changed == 'true' }}
enable-markdown-lint: ${{ needs.check-changes.outputs.check-all == 'true' || needs.check-changes.outputs.markdown-changed == 'true' }}
perl-files: ${{ needs.check-changes.outputs.perl-files }}
markdown-files: ${{ needs.check-changes.outputs.markdown-files }}
test:
name: Tests
needs: check-changes
if: |
(needs.check-changes.outputs.check-all == 'true') ||
(needs.check-changes.outputs.perl-changed == 'true' || needs.check-changes.outputs.test-changed == 'true')
uses: ./.github/workflows/tests.yml
+166
View File
@@ -0,0 +1,166 @@
name: Lint
run-name: Lint ${{ inputs.enable-perl-lint && 'Perl' || '' }} ${{ inputs.enable-markdown-lint && 'Markdown' || '' }}
on:
workflow_call:
inputs:
enable-perl-lint:
description: 'Enable Perl linting'
required: false
type: boolean
default: false
enable-markdown-lint:
description: 'Enable Markdown linting'
required: false
type: boolean
default: false
perl-files:
description: 'List of changed Perl files'
required: false
type: string
default: ''
markdown-files:
description: 'List of changed Markdown files'
required: false
type: string
default: ''
jobs:
perl-lint:
name: Perl Lint
if: ${{ inputs.enable-perl-lint == true }}
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: '5.34'
- name: Install cpanm
run: |
curl -L https://cpanmin.us | perl - App::cpanminus
- name: Install linting tools
run: |
cpanm --notest Perl::Critic
sudo apt-get update
sudo apt-get install -y perltidy || true
- name: Check code formatting with perltidy
if: inputs.perl-files != ''
run: |
echo "Checking code formatting..."
changed_files="${{ inputs.perl-files }}"
if [ -z "$changed_files" ] || [ "$changed_files" == " " ]; then
echo "No Perl files to check"
exit 0
fi
if [ ! -f ".perltidyrc" ]; then
echo "Warning: .perltidyrc not found, using default perltidy settings"
fi
for file in $changed_files; do
if [[ -n "$file" ]] && ([[ "$file" == *.pm ]] || [[ "$file" == *.pl ]]); then
echo "Checking formatting: $file"
if command -v perltidy >/dev/null 2>&1; then
formatted_file="${file}.formatted"
if [ -f ".perltidyrc" ]; then
perltidy -pro=.perltidyrc "$file" -o "$formatted_file" 2>&1 || true
else
perltidy "$file" -o "$formatted_file" 2>&1 || true
fi
if [ -f "$formatted_file" ]; then
if ! diff -q "$file" "$formatted_file" >/dev/null 2>&1; then
echo "::error file=$file::File is not properly formatted. Run 'perltidy -pro=.perltidyrc $file' to fix."
echo "Differences:"
diff "$file" "$formatted_file" | head -20 || true
rm -f "$formatted_file"
exit 1
fi
rm -f "$formatted_file"
else
echo "Warning: perltidy failed to create formatted file for $file"
fi
else
echo "Warning: perltidy not found, skipping formatting check"
fi
fi
done
find . -name '*.formatted' -delete 2>/dev/null || true
- name: Run Perl::Critic (optional)
if: inputs.perl-files != ''
continue-on-error: true
run: |
echo "Running Perl::Critic static analysis..."
if command -v perlcritic >/dev/null 2>&1; then
changed_files="${{ inputs.perl-files }}"
if [ -z "$changed_files" ] || [ "$changed_files" == " " ]; then
echo "No Perl files to analyze"
exit 0
fi
for file in $changed_files; do
if [[ -n "$file" ]] && [[ "$file" == *.pm ]]; then
echo "Analyzing: $file"
perlcritic --quiet "$file" || true
fi
done
else
echo "Perl::Critic not available, skipping..."
fi
markdown-lint:
name: Markdown Lint
if: ${{ inputs.enable-markdown-lint == true }}
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install markdownlint
run: |
npm install -g markdownlint-cli2 || true
- name: Lint Markdown files
if: inputs.markdown-files != ''
run: |
echo "Linting Markdown files..."
changed_files="${{ inputs.markdown-files }}"
if [ -z "$changed_files" ] || [ "$changed_files" == " " ]; then
echo "No Markdown files to lint"
exit 0
fi
if command -v markdownlint-cli2 >/dev/null 2>&1; then
for file in $changed_files; do
if [[ -n "$file" ]] && [[ "$file" == *.md ]]; then
echo "Linting: $file"
markdownlint-cli2 "$file" "#node_modules" "#.git" || {
echo "::error file=$file::Markdown linting failed"
exit 1
}
fi
done
elif command -v markdownlint >/dev/null 2>&1; then
for file in $changed_files; do
if [[ -n "$file" ]] && [[ "$file" == *.md ]]; then
echo "Linting: $file"
markdownlint "$file" || {
echo "::error file=$file::Markdown linting failed"
exit 1
}
fi
done
else
echo "Markdown linter not available, skipping..."
fi
+33
View File
@@ -0,0 +1,33 @@
name: Tests
run-name: Tests - ${{ github.ref_name }}
on:
workflow_call:
jobs:
test:
name: Run tests
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Perl
uses: shogo82148/actions-setup-perl@v1
with:
perl-version: '5.34'
- name: Install cpanm
run: |
curl -L https://cpanmin.us | perl - App::cpanminus
- name: Install Perl dependencies
run: |
cpanm --notest Test::More JSON JSON::XS
- name: Run tests
run: |
chmod +x tests/run_tests.sh
./tests/run_tests.sh
+3
View File
@@ -5,7 +5,10 @@ debian/libpve-storage-purestorage-perl
debian/files debian/files
debian/package debian/package
debian/tmp debian/tmp
tests/test_results.txt
/*.buildinfo /*.buildinfo
/*.deb /*.deb
*.log *.log
*.substvars *.substvars
build/
changelog
+6
View File
@@ -0,0 +1,6 @@
{
"MD013": {
"tables": false,
"code_blocks": false
}
}
+1288 -364
View File
File diff suppressed because it is too large Load Diff
+532 -52
View File
@@ -1,29 +1,79 @@
# Proxmox VE Plugin for Pure Storage as Multipath iSCSI Source # Proxmox VE Plugin for Pure Storage as Multipath iSCSI Source
This plugin enables the integration of Pure Storage arrays with Proxmox Virtual Environment (VE) using multipath iSCSI or Fibre Channel (FC). It allows you to use Pure Storage as a backend for your virtual machine disks, providing high performance and reliability. [![Checks](https://github.com/kolesa-team/pve-purestorage-plugin/actions/workflows/checks.yml/badge.svg)](https://github.com/kolesa-team/pve-purestorage-plugin/actions/workflows/checks.yml)
This plugin enables the integration of Pure Storage arrays with Proxmox
Virtual Environment (VE) using multipath iSCSI or Fibre Channel (FC).
It allows you to use Pure Storage as a backend for your virtual machine
disks, and optionally for LXC container root volumes when configured,
providing high performance and reliability.
## Table of Contents ## Table of Contents
- [Features](#features)
- [Linux containers (LXC)](#linux-containers-lxc)
- [Prerequisites](#prerequisites) - [Prerequisites](#prerequisites)
- [Multipath Configuration](#multipath-configuration) - [Multipath Configuration](#multipath-configuration)
- [iSCSI Configuration](#iscsi-configuration)
- [Installation](#installation) - [Installation](#installation)
- [Manual](#manual) - [Manual Installation](#manual-installation)
- [APT](#apt) - [Debian Package Installation (Recommended)](#debian-package-installation-recommended)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Debug Logging](#debug-logging)
- [Service Status](#service-status)
- [Diagnostic Commands](#diagnostic-commands)
- [Known issues](#known-issues)
- [Contributing](#contributing) - [Contributing](#contributing)
## Features
- Easily enable and configure multipathing iSCSI to the Pure Array
- **Active Cluster support (Experimental)** - Automatic volume connection
on both arrays in Active Cluster configuration
- Volumes are automatically connected to hosts on both primary and
secondary arrays
- Ensures high availability and optimal connectivity in Active Cluster
setups
- Storage based snapshots
- Snapshots are presented in Proxmox like any other native Snapshot to a VM
- Snapshots are created by the Pure Array, making them deduped and instant
- Instant storage migration
- The plugin will automatically map the iSCSI volumes needed on the
host the VM is being migrated to
- Optional **LXC** support — include `rootdir` in `content` to allow raw
image-backed container root disks on the same block path as QEMU disks
(see [Linux containers (LXC)](#linux-containers-lxc))
## Linux containers (LXC)
The plugin exposes the Proxmox **`rootdir`** content type so a storage
section can host **LXC root filesystems** as raw volumes, alongside
`images` (QEMU disks), when you set e.g. `content images,rootdir`.
LXC on this backend uses the same allocation and device path logic as VM
disks. **Support is best-effort and community-tested** — validate create,
resize, and delete for your workloads on a real array before relying on it
in production. If you only need QEMU VMs, keep `content images` (omit
`rootdir`).
## Prerequisites ## Prerequisites
Before installing and using this plugin, ensure that your Proxmox VE environment meets the following prerequisites. Before installing and using this plugin, ensure that your Proxmox VE
environment meets the following prerequisites.
### Multipath Configuration ### Multipath Configuration
To ensure correct operation with Pure Storage, you need to configure your multipath settings appropriately. Specifically, you need to set find_multipaths to no in your multipath.conf file. This setting disables the automatic detection of multipath devices, which is necessary for Pure Storage devices to be correctly recognized. To ensure correct operation with Pure Storage, you need to configure your
multipath settings appropriately. Specifically, you need to set
find_multipaths to no in your multipath.conf file. This setting disables
the automatic detection of multipath devices, which is necessary for Pure
Storage devices to be correctly recognized.
Below is an example of how your multipath.conf file should look when configured for Pure Storage arrays: Below is an example of how your multipath.conf file should look when
configured for Pure Storage arrays:
``` ```text
defaults { defaults {
polling_interval 2 polling_interval 2
find_multipaths no find_multipaths no
@@ -63,54 +113,152 @@ blacklist_exceptions {
} }
``` ```
## Installation ### iSCSI Configuration
There are two methods to install the plugin: manual installation and APT package installation. Initiate iSCSI according to the Proxmox Guidelines.
### Manual
To manually install the plugin, follow these steps:
```bash ```bash
# Clone repository sudo iscsiadm -m discovery -t sendtargets -p <PURE ISCSI ADAPTER IP>
git clone git@github.com:kolesa-team/pve-purestorage.git sudo iscsiadm -m node --op update -n node.startup -v automatic
# Navigate to the Plugin Directory
cd pve-purestorage
# Create the custom plugin directory if it does not already exist
mkdir /usr/share/perl5/PVE/Storage/Custom
# Copy plugin to custom plugin directory
sudo cp PureStoragePlugin.pm /usr/share/perl5/PVE/Storage/Custom/PureStoragePlugin.pm
# Restart Proxmox VE
sudo systemctl restart pve-cluster.service pvedaemon.service pvestatd.service pveproxy.service pvescheduler.service
``` ```
### APT > [!CAUTION]
> As long as there are no hostX entries in /sys/class/iscsi_host/ the
> plugin is not ready to be used.
**Note**: Replace `<PACKAGE_VERSION>` with your desired version number (e.g., `0.0.1`). ## Installation
There are two methods to install the plugin: manual installation and Debian
package installation via APT.
> [!IMPORTANT]
> If you are using a cluster setup, install the plugin on all nodes in the
> cluster. The storage configuration will be automatically synchronized via
> corosync, but the plugin code must be present on each node.
### Manual Installation
Manual installation is useful for development or when you want to install
from source.
#### Step 1: Install required dependencies
```bash ```bash
PACKAGE_VERSION="<PACKAGE_VERSION>" curl -L -o libpve-storage-purestorage-perl.deb "https://github.com/kolesa-team/pve-purestorage-plugin/releases/download/v$PACKAGE_VERSION/libpve-storage-purestorage-perl_$PACKAGE_VERSION-1_all.deb" sudo apt-get update
sudo apt-get install -y \
libwww-perl \
libjson-perl \
libjson-xs-perl
```
sudo apt install ./libpve-storage-purestorage-perl.deb #### Step 2: Clone the repository
```bash
git clone https://github.com/kolesa-team/pve-purestorage-plugin.git
cd pve-purestorage-plugin
```
#### Step 3: Install the plugin
```bash
# Create the custom plugin directory
sudo mkdir -p /usr/share/perl5/PVE/Storage/Custom
# Copy plugin file
sudo cp PureStoragePlugin.pm /usr/share/perl5/PVE/Storage/Custom/PureStoragePlugin.pm
# Set correct permissions
sudo chmod 644 /usr/share/perl5/PVE/Storage/Custom/PureStoragePlugin.pm
```
#### Step 4: Restart Proxmox VE services
```bash
sudo systemctl restart pvedaemon.service pveproxy.service
```
#### Step 5: Verify installation
```bash
pvesm status
# The purestorage type should now be available
```
### Debian Package Installation (Recommended)
Installing via Debian package is the recommended method as it handles
dependencies automatically and provides easy updates.
#### Step 1: Download the package*
Replace `<PACKAGE_VERSION>` with the desired version (e.g., `0.0.1`). Check
the [releases page](https://github.com/kolesa-team/pve-purestorage-plugin/releases)
for available versions.
```bash
PACKAGE_VERSION="<PACKAGE_VERSION>"
wget "https://github.com/kolesa-team/pve-purestorage-plugin/releases/download/v${PACKAGE_VERSION}/libpve-storage-purestorage-perl_${PACKAGE_VERSION}-1_all.deb"
```
#### Step 2: Install the package
```bash
sudo apt install ./libpve-storage-purestorage-perl_${PACKAGE_VERSION}-1_all.deb
```
#### Step 3: Verify installation
```bash
dpkg -l | grep libpve-storage-purestorage-perl
# Should show the installed package version
```
#### To upgrade to a newer version
```bash
# Download new version
PACKAGE_VERSION="<NEW_VERSION>"
wget "https://github.com/kolesa-team/pve-purestorage-plugin/releases/download/v${PACKAGE_VERSION}/libpve-storage-purestorage-perl_${PACKAGE_VERSION}-1_all.deb"
# Upgrade
sudo apt install ./libpve-storage-purestorage-perl_${PACKAGE_VERSION}-1_all.deb
```
#### To uninstall
```bash
sudo apt remove libpve-storage-purestorage-perl
``` ```
## Configuration ## Configuration
After installing the plugin, you need to configure Proxmox VE to use it. Since Proxmox VE does not currently support adding custom storage plugins via the GUI, you will need to open shell and use `pvesm` command to add it: > [!TIP]
> If you are using a cluster setup - this step needs to be executed only
> on one node of the cluster - corosync will do the rest for you.
After installing the plugin, you need to configure Proxmox VE to use it.
Since Proxmox VE does not currently support adding custom storage plugins
via the GUI, you will need to open shell and use `pvesm` command to add it:
```bash ```bash
pvesm add purestorage <storage_id> \ pvesm add purestorage <storage_id> \
--nodes <proxmox_node_list> \ --nodes <proxmox_node_list> \
--address https://<purestorage_fqdn_or_ip> \ --address \
--token token <purestorage_api_token> \ https://<purestorage_fqdn_or_ip> \
--token <purestorage_api_token> \
--vgname <purestorage_volume_group_name> \ --vgname <purestorage_volume_group_name> \
--hgsuffix <purestorage_host_suffix> --hgsuffix <purestorage_host_suffix>
--content images --content images
``` ```
Alternatively, you can manually edit the storage configuration file `/etc/pve/storage.cfg`. To allow **LXC container root disks** on the same pool as well, use
`--content images,rootdir` instead of `--content images` (see
[Linux containers (LXC)](#linux-containers-lxc)).
``` Alternatively, you can manually edit the storage configuration file
`/etc/pve/storage.cfg`.
```text
purestorage: <storage_id> purestorage: <storage_id>
nodes <proxmox_node_list> nodes <proxmox_node_list>
address https://<purestorage_fqdn_or_ip> address https://<purestorage_fqdn_or_ip>
@@ -124,60 +272,392 @@ purestorage: <storage_id>
| --------- | ----------- | | --------- | ----------- |
| storage_id | The storage identifier (name under which it will appear in the Storage list) | | storage_id | The storage identifier (name under which it will appear in the Storage list) |
| nodes | (`optional`) A comma-separated list of Proxmox node names. Use this parameter to limit the plugin to specific nodes in your cluster. If omitted, the storage is available to all nodes. | | nodes | (`optional`) A comma-separated list of Proxmox node names. Use this parameter to limit the plugin to specific nodes in your cluster. If omitted, the storage is available to all nodes. |
| address | The URL or IP address of the Pure Storage API endpoint. Ensure that the Proxmox VE nodes can reach this address over the network. | | address | The URL or IP address of the Pure Storage API endpoint. Ensure that the Proxmox VE nodes can reach this address over the network. For high availability or Active Cluster configuration (experimental), you can specify multiple arrays separated by commas (e.g., `https://array1.example.com,https://array2.example.com`). When multiple arrays are specified, the plugin automatically connects volumes to hosts on all arrays. |
| token | The API token used for authentication with the Pure Storage array. This token must have sufficient permissions to create and manage volumes. | | token | The API token used for authentication with the Pure Storage array. This token must have sufficient permissions to create and manage volumes. For multiple arrays, specify tokens separated by commas in the same order as addresses. Each token must have permissions for its corresponding array. |
| vgname | (`optional`, conflicts with `podname`) The volume group name where virtual disks will be stored. This should match the configuration on your Pure Storage array. | | vgname | (`optional`, conflicts with `podname`) The volume group name where virtual disks will be stored. This should match the configuration on your Pure Storage array. |
| podname | (`optional`, conflicts with `vgname`) The pod name where virtual disks will be stored. This should match the configuration on your Pure Storage array. | | podname | (`optional`, conflicts with `vgname`) The pod name where virtual disks will be stored. This should match the configuration on your Pure Storage array. |
| vnprefix | (`optional`) The prefix to prepend to name of virtual disks. | | vnprefix | (`optional`) The prefix to prepend to name of virtual disks. |
| hgsuffix | (`optional`) A suffix that is appended to the hostname when the plugin interacts with the Pure Storage array. This can help differentiate hosts if necessary. | | hgsuffix | (`optional`) A suffix that is appended to the hostname when the plugin interacts with the Pure Storage array. This can help differentiate hosts if necessary. |
| content | Specifies the types of content that can be stored. For virtual machine disk images, use images. | | content | Specifies the types of content that can be stored. Use `images` for QEMU VM disks. Add `rootdir` (e.g. `images,rootdir`) to allow LXC root volumes on the same pool; see [Linux containers (LXC)](#linux-containers-lxc). |
| protocol | (`optional`, default is `iscsi`) Specifies the storage protocol (`iscsi`, `fc`). |
| check_ssl | (`optional`, default is `no`) Verify the server's TLS certificate. Set to `yes` to enable SSL certificate verification. |
| token_ttl | (`optional`, default is `3600`) Session token time-to-live in seconds. The plugin caches PureStorage API session tokens in `/etc/pve/priv/purestorage/` (automatically replicated across cluster nodes). Tokens are proactively refreshed at 80% of TTL to prevent expiration during operations. |
| debug | (`optional`, default is `0`) Enable debug logging. Levels: 0=off, 1=basic (token operations, main calls), 2=verbose (HTTP details, validation), 3=trace (all internals). Environment variable `PURESTORAGE_DEBUG` can be used as fallback when `debug` is not set in config. |
> **_NOTE:_** Ensure that the token and other sensitive information are kept secure and not exposed publicly. > **_NOTE:_** Ensure that the token and other sensitive information are
> kept secure and not exposed publicly.
Example Configuration: Example Configuration:
``` **Single Array:**
```text
purestorage: pure purestorage: pure
address https://purestorage.example.com address https://purestorage.example.com
token abc123 token abc123
vgname pure_vg vgname pure_vg
hgsuffix ""
content images content images
``` ```
**Active Cluster (Multiple Arrays) - Experimental:**
```text
purestorage: pure-cluster
address https://array1.example.com,https://array2.example.com
token token1,token2
vgname pure_vg
content images
```
**LXC-capable pool** (`images` and `rootdir`; best-effort — see
[Linux containers (LXC)](#linux-containers-lxc)):
```text
purestorage: pure-lxc
address https://purestorage.example.com
token abc123
vgname pure_vg
hgsuffix ""
content images,rootdir
```
> [!NOTE]
> When multiple arrays are specified (Active Cluster configuration -
> experimental feature), the plugin automatically connects volumes to hosts
> on both arrays. This ensures high availability - if one array fails,
> volumes remain accessible through the other array. The plugin handles
> connection management on all arrays transparently.
## Troubleshooting ## Troubleshooting
If you encounter issues while using the plugin, consider the following steps: If you encounter issues while using the plugin, consider the following
steps:
- Check Service Status: Ensure that the Proxmox VE services are running correctly. You can restart the services if necessary: ### Debug Logging
The plugin provides detailed debug logging to help diagnose issues. Debug
output is written to syslog and can be viewed in Proxmox logs.
**Enable debug logging:**
Persistent (via configuration):
```bash
pvesm set <storage_id> --debug 1
```
Temporary (for single command, when debug is not set in config):
```bash
PURESTORAGE_DEBUG=1 pvesm list <storage_id>
```
> **Note:** If `debug` is set in storage configuration, it takes priority
> over `PURESTORAGE_DEBUG` environment variable.
**Debug levels:**
- `0` - Off (production, default)
- `1` - Basic (token operations, main function calls, volume operations)
- `2` - Verbose (HTTP requests, token validation, API responses)
- `3` - Trace (all internal operations, detailed flow)
**Example debug output:**
```bash
PURESTORAGE_DEBUG=1 pvesm list pure-n1
Debug :: activate_storage (pure-n1)
Debug :: list_images (pure-n1, vmid=all)
Debug :: Read token cache from: /etc/pve/priv/purestorage/pure-n1_array0.json
Debug :: Token is valid (age: 125s)
Debug :: Using cached token from file (age: 125s)
```
**Common debug scenarios:**
Debug volume creation:
```bash
PURESTORAGE_DEBUG=2 pvesm alloc <storage_id> <vmid> <volname> 10G
```
Debug volume deletion:
```bash
PURESTORAGE_DEBUG=2 pvesm free <storage_id>:<volname>
```
Debug API authentication issues:
```bash
PURESTORAGE_DEBUG=3 pvesm status <storage_id>
```
**View debug logs:**
Check Proxmox daemon logs:
```bash
journalctl -u pvedaemon -f
```
Filter for PureStorage plugin messages:
```bash
journalctl -u pvedaemon | grep -E "(Debug ::|Info ::|Warning ::|Error ::)"
```
Check token cache status:
```bash
ls -lah /etc/pve/priv/purestorage/
cat /etc/pve/priv/purestorage/<storage_id>_array0.json | jq .
```
### Service Status
Ensure that the Proxmox VE services are running correctly. You can restart
the services if necessary:
```bash ```bash
sudo systemctl restart pve-cluster.service pvedaemon.service pvestatd.service pveproxy.service pvescheduler.service sudo systemctl restart pve-cluster.service pvedaemon.service pvestatd.service pveproxy.service pvescheduler.service
``` ```
- Verify Network Connectivity: Ensure that the Proxmox VE nodes can reach the Pure Storage array over the network. Check for firewall rules or network issues that might be blocking communication. ### Diagnostic Commands
- Review Logs: Check the Proxmox VE logs for any error messages related to storage or the plugin. Logs are typically found in /var/log/pve.
These commands are helpful for troubleshooting: **Multipath diagnostics:**
```bash
multipath -ll -v3 #diagnose issues with the multipath service List all multipath devices with details:
iscsiadm -m node #list what iscsi nodes are mounted
ls -l /dev/mapper/3624a9370* #list wwids of Pure mapped devices on the system ```bash
``` multipath -ll
- Multipath Configuration: Verify that your multipath.conf is correctly configured and that multipath devices are recognized. Use multipath -ll to list the current multipath devices. ```
- API Token Permissions: Ensure that the API token used has the necessary permissions to create and manage volumes on the Pure Storage array.
- Plugin Updates: Ensure you are using the latest version of the plugin. Check the GitHub repository for updates. Verbose multipath debugging:
```bash
multipath -ll -v3
```
Show only Pure Storage devices:
```bash
multipath -ll | grep -A 10 "3624a9370"
```
Reload multipath configuration:
```bash
systemctl reload multipathd
```
**iSCSI diagnostics:**
List all iSCSI sessions:
```bash
iscsiadm -m session
```
Show detailed session information:
```bash
iscsiadm -m session -P 3
```
List all discovered targets:
```bash
iscsiadm -m node
```
Rescan iSCSI sessions:
```bash
iscsiadm -m session --rescan
```
**PureStorage volume diagnostics:**
List all Pure Storage mapped devices:
```bash
ls -l /dev/mapper/3624a9370*
```
Show device information:
```bash
lsblk | grep "3624a9370"
```
Check device WWIDs:
```bash
/lib/udev/scsi_id --whitelisted --device=/dev/mapper/3624a9370<wwid>
```
Show device mapper table:
```bash
dmsetup table
```
Show device mapper dependencies:
```bash
dmsetup deps -o devname
```
List all device mapper devices:
```bash
dmsetup ls --tree
```
Show detailed info for specific device:
```bash
dmsetup info /dev/mapper/3624a9370<wwid>
```
**Partition management (kpartx):**
List partitions on a device:
```bash
kpartx -l /dev/mapper/3624a9370<wwid>
```
Add partition mappings:
```bash
kpartx -a /dev/mapper/3624a9370<wwid>
```
Remove partition mappings:
```bash
kpartx -d /dev/mapper/3624a9370<wwid>
```
Sync partition table:
```bash
kpartx -u /dev/mapper/3624a9370<wwid>
```
**Storage plugin diagnostics:**
List all volumes on storage:
```bash
pvesm list <storage_id>
```
Show storage status:
```bash
pvesm status <storage_id>
```
Scan for new volumes:
```bash
pvesm scan <storage_id>
```
Test volume allocation (dry-run):
```bash
pvesm alloc <storage_id> <vmid> test-volume 1G
pvesm free <storage_id>:test-volume
```
**Network connectivity:**
Test API endpoint connectivity:
```bash
curl -k https://<array_address>/api/2.30/arrays
```
Test with API token:
```bash
curl -k -X POST https://<array_address>/api/2.30/login \
-H "Content-Type: application/json" \
-d '{"api_token":"<your_token>"}'
```
Check iSCSI portal connectivity:
```bash
nc -zv <array_iscsi_ip> 3260
```
**Common issues:**
- **API Token Permissions**: Ensure the API token has sufficient permissions
to create and manage volumes on the Pure Storage array
- **Multipath Configuration**: Verify multipath.conf is correctly configured
and multipath devices are recognized
- **Network Connectivity**: Check firewall rules and network routes to Pure
Storage array
- **Plugin Updates**: Ensure you are using the latest version of the plugin
### Known issues ### Known issues
- `lvm inside a volume`: If you plan to use LVM inside a volume, it is better to add purestorage volumes to the ignore list to avoid scanning. **LVM inside a volume:**
If you plan to use LVM inside a volume, it is better to add purestorage
volumes to the ignore list to avoid scanning.
```bash ```bash
cat /etc/lvm/lvmlocal.conf cat /etc/lvm/lvmlocal.conf
... ...
devices { devices {
global_filter=["r|/dev/zd.*|","r|/dev/rbd.*|","r|/dev/mapper/3624a9370.*|"] global_filter=["r|/dev/zd.*|","r|/dev/rbd.*|",
"r|/dev/mapper/3624a9370.*|"]
} }
``` ```
**Debug output contamination ([#56](https://github.com/kolesa-team/pve-purestorage-plugin/issues/56)):**
When debug logging is enabled, debug messages may contaminate command
outputs that should be clean (e.g., `qm showcmd`, `pvesm path`). This can
break tools that parse these outputs.
Workaround: Disable debug logging when using commands that need clean output:
```bash
pvesm set <storage_id> --debug 0
```
Or use temporary debug only when needed:
```bash
PURESTORAGE_DEBUG=1 <command>
```
**Volume auto-mount issues ([#59](https://github.com/kolesa-team/pve-purestorage-plugin/issues/59)):**
The `filesystem_path` function returns device paths that may not work
reliably when volumes are deactivated. This affects operations like
`qm showcmd` and backup tools (e.g., Veeam) that need direct access to
volume paths.
Current behavior: Volumes are activated on-demand and may deactivate when
not in use.
Workaround: Ensure volumes are activated before accessing them directly.
For automated workflows, consider implementing volume activation in your
scripts.
Note: A proper solution using autofs for automatic volume mounting is being
evaluated.
## Contributing ## Contributing
Contributions to this project are welcome. Contributions to this project are welcome.
+8 -1
View File
@@ -10,10 +10,17 @@ Build-Depends: debhelper, devscripts
Package: libpve-storage-purestorage-perl Package: libpve-storage-purestorage-perl
Architecture: all Architecture: all
Depends: ${perl:Depends}, ${misc:Depends}, libpve-storage-perl (>= 8.2.1), multipath-tools Depends: ${perl:Depends}, ${misc:Depends},
libpve-storage-perl (>= 8.2.1),
multipath-tools,
libjson-xs-perl,
libwww-perl,
liburi-perl
Description: Pure Storage integration library for Proxmox VE Description: Pure Storage integration library for Proxmox VE
This package provides a Perl library to integrate Pure FlashArray Storage systems This package provides a Perl library to integrate Pure FlashArray Storage systems
with the Proxmox VE storage management infrastructure. with the Proxmox VE storage management infrastructure.
. .
It allows administrators to configure and manage Pure FlashArray Storage devices It allows administrators to configure and manage Pure FlashArray Storage devices
directly within the Proxmox VE environment. directly within the Proxmox VE environment.
.
Supports Proxmox VE 8.2+ and Proxmox VE 9+.
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
set -e
# Build Debian package locally using Docker
# This script mimics the CI/CD build process for local testing
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
BUILD_IMAGE="debian:bookworm"
echo "Building Debian package locally..."
# Check if Docker is available
if ! command -v docker &> /dev/null; then
echo "Error: Docker is not installed or not in PATH"
exit 1
fi
# Run build in Docker container
docker run --rm \
-v "$PROJECT_DIR:/workspace" \
-w /workspace \
"$BUILD_IMAGE" \
bash -c '
set -e
echo "Installing build dependencies..."
apt-get update -qq
apt-get install -y -qq \
debhelper \
devscripts \
build-essential \
git
echo "Installing package build dependencies..."
apt-get build-dep -y . || true
echo "Generating changelog..."
pkgname=libpve-storage-purestorage-perl
tag_list=$(git tag -l | grep -ve "-rc\.[0-9]$" | grep -ve "-beta\.[0-9]$" | sort -V)
if [ -z "$tag_list" ]; then
echo "Warning: No tags found, using default version"
echo "$pkgname (0.0.1-1) stable; urgency=medium" > debian/changelog
echo "" >> debian/changelog
echo " * Initial release" >> debian/changelog
echo "" >> debian/changelog
echo " -- Local Build <build@localhost> $(date -R)" >> debian/changelog
else
prevtag=""
for tag in ${tag_list}; do
tag_header="$tag^..$tag"
tag_info=$prevtag..$tag
tag_version=$(echo $tag | cut -c2-)-1
if [[ "$prevtag" == "" ]]; then
tag_header="$tag"
tag_info="$tag"
elif [[ "$tag" == "$prevtag" ]]; then
continue
fi
echo >> changelog
git log --pretty="format: -- %aN <%aE> %aD%n%n" $tag_header >> changelog
git log --pretty=format:" * %s%n" $tag_info >> changelog
echo "" >> changelog
echo "$pkgname ($tag_version) stable; urgency=medium" >> changelog
prevtag=$tag
done
tac changelog > debian/changelog
fi
echo "Building package..."
dpkg-buildpackage -us -uc -b
echo "Generating checksums..."
cd ..
sha256sum *.deb > sha256sums 2>/dev/null || true
echo "Moving files to build output directory..."
mkdir -p /workspace/build
mv *.deb *.buildinfo *.changes sha256sums /workspace/build/ 2>/dev/null || true
echo "Build complete!"
echo ""
echo "Generated files:"
ls -lh /workspace/build/
'
echo ""
echo "Package built successfully!"
echo "Output files are in the build/ directory:"
ls -lh "$PROJECT_DIR"/build/*.deb 2>/dev/null || echo "No .deb files found"
+31
View File
@@ -0,0 +1,31 @@
# PureStorage Plugin Tests
This directory contains tests for the PVE PureStorage Plugin.
## Test Structure
- `unit/` - Unit tests for individual functions
- `integration/` - Integration tests requiring actual PureStorage array
- `fixtures/` - Test data and mock responses
- `scripts/` - Helper scripts for testing
## Running Tests
```bash
# Run all unit tests
prove -v tests/unit/
# Run specific test
perl tests/unit/test_token_cache.t
# Run with verbose output
perl -I. tests/unit/test_command_validation.t
```
## Test Coverage
- Token caching and expiration
- Command path validation
- API request/response handling
- Device cleanup functions
- Error handling
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
echo "==================================="
echo "PureStorage Plugin Test Suite"
echo "==================================="
echo ""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check if Test::More is available
if ! perl -MTest::More -e 'print "OK\n"' 2>/dev/null | grep -q OK; then
echo -e "${RED}Error: Test::More module not found${NC}"
echo "Please install it with: cpan Test::More"
exit 1
fi
# Check if JSON is available
if ! perl -MJSON -e 'print "OK\n"' 2>/dev/null | grep -q OK; then
echo -e "${YELLOW}Warning: JSON module not found${NC}"
echo "Some tests may fail. Install with: cpan JSON"
fi
echo -e "${GREEN}Running Unit Tests${NC}"
echo "-----------------------------------"
failed_tests=0
total_tests=0
# Run all test files in tests/unit/
for test_file in tests/unit/*.t; do
if [ -f "$test_file" ]; then
total_tests=$((total_tests + 1))
echo ""
echo -e "${YELLOW}Running: $(basename $test_file)${NC}"
if perl -I. "$test_file"; then
echo -e "${GREEN}✓ PASS${NC}"
else
echo -e "${RED}✗ FAIL${NC}"
failed_tests=$((failed_tests + 1))
fi
fi
done
echo ""
echo "==================================="
echo "Test Summary"
echo "==================================="
echo "Total tests: $total_tests"
echo -e "${GREEN}Passed: $((total_tests - failed_tests))${NC}"
if [ $failed_tests -gt 0 ]; then
echo -e "${RED}Failed: $failed_tests${NC}"
exit 1
else
echo -e "${GREEN}All tests passed!${NC}"
exit 0
fi
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More tests => 15;
use File::Temp qw( tempdir );
use JSON::XS qw( encode_json decode_json );
# Mock PVE::Tools for testing
BEGIN {
package PVE::Tools;
use Exporter 'import';
our @EXPORT_OK = qw( file_get_contents );
sub file_get_contents {
my ( $path ) = @_;
open my $fh, '<', $path or die "Cannot read $path: $!";
local $/;
my $content = <$fh>;
close $fh;
return $content;
}
}
# Test token cache implementation
package main;
my $test_dir = tempdir( CLEANUP => 1 );
my $cache_path = "$test_dir/test_cache.json";
# Helper function to create mock token data
sub create_token_data {
my ( $age ) = @_;
my $now = time();
return {
auth_token => 'test-token-' . int( rand( 1000 ) ),
request_id => 'test-request-id',
created_at => $now - $age,
ttl => 3600,
expires_at => $now - $age + 3600
};
}
# Helper function to write token cache
sub write_test_cache {
my ( $token_data ) = @_;
my $json = encode_json( $token_data );
open my $fh, '>', $cache_path or die "Cannot write cache: $!";
print $fh $json;
close $fh;
}
# Test 1: Token validation - fresh token
{
my $token_data = create_token_data( 100 ); # 100s old
my $ttl = 3600;
my $threshold = $ttl * 0.8; # 2880s
ok( 100 < $threshold, 'Fresh token is valid (age < 80% TTL)' );
}
# Test 2: Token validation - expired token
{
my $token_data = create_token_data( 3000 ); # 3000s old
my $ttl = 3600;
my $threshold = $ttl * 0.8; # 2880s
ok( 3000 >= $threshold, 'Expired token needs refresh (age >= 80% TTL)' );
}
# Test 3: Cache file write and read
{
my $token_data = create_token_data( 50 );
write_test_cache( $token_data );
ok( -f $cache_path, 'Cache file created' );
my $json_text = PVE::Tools::file_get_contents( $cache_path );
my $read_data = decode_json( $json_text );
is( $read_data->{ auth_token }, $token_data->{ auth_token }, 'Token data matches after read' );
}
# Test 4: Cache file validation - valid token
{
my $token_data = create_token_data( 100 );
write_test_cache( $token_data );
my $json_text = PVE::Tools::file_get_contents( $cache_path );
my $cached = decode_json( $json_text );
my $age = time() - $cached->{ created_at };
my $threshold = 3600 * 0.8;
ok( $age < $threshold, 'Cached token is still valid' );
}
# Test 5: Cache file validation - expired token
{
my $token_data = create_token_data( 3000 );
write_test_cache( $token_data );
my $json_text = PVE::Tools::file_get_contents( $cache_path );
my $cached = decode_json( $json_text );
my $age = time() - $cached->{ created_at };
my $threshold = 3600 * 0.8;
ok( $age >= $threshold, 'Cached token is expired and should be refreshed' );
}
# Test 6: Race condition mitigation - newer token exists
{
my $old_token = create_token_data( 200 );
my $new_token = create_token_data( 50 );
ok( $new_token->{ created_at } > $old_token->{ created_at }, 'Newer token has later created_at timestamp' );
}
# Test 7: TTL validation
{
my $ttl = 3600;
my $refresh_threshold = $ttl * 0.8;
is( $refresh_threshold, 2880, 'Refresh threshold is 80% of TTL' );
# Test jitter range (±2.5%)
my $jitter_min = $ttl * ( 0.8 - 0.025 );
my $jitter_max = $ttl * ( 0.8 + 0.025 );
ok( $jitter_min < $refresh_threshold && $refresh_threshold < $jitter_max, 'Jitter keeps threshold within ±2.5% of 80% TTL' );
}
# Test 8: Multiple token files
{
my $cache1 = "$test_dir/storage1_array0.json";
my $cache2 = "$test_dir/storage2_array0.json";
my $token1 = create_token_data( 100 );
my $token2 = create_token_data( 200 );
open my $fh1, '>', $cache1 or die $!;
print $fh1 encode_json( $token1 );
close $fh1;
open my $fh2, '>', $cache2 or die $!;
print $fh2 encode_json( $token2 );
close $fh2;
ok( -f $cache1 && -f $cache2, 'Multiple cache files can coexist' );
}
# Test 9: Token cache path generation
{
my $storeid = 'pure-n1';
my $array_index = 0;
my $expected_path = "/etc/pve/priv/purestorage/${storeid}_array${array_index}.json";
like( $expected_path, qr/\/etc\/pve\/priv\/purestorage\/pure-n1_array0\.json$/, 'Cache path follows expected format' );
}
# Test 10: Atomic write simulation
{
my $temp_path = "$cache_path.tmp.$$";
my $token_data = create_token_data( 75 );
# Write to temp file
open my $fh, '>', $temp_path or die $!;
print $fh encode_json( $token_data );
close $fh;
ok( -f $temp_path, 'Temp file created' );
# Atomic rename
rename( $temp_path, $cache_path ) or die "Cannot rename: $!";
ok( -f $cache_path && !-f $temp_path, 'Atomic rename completed' );
}
# Test 11: Concurrent token creation scenario
{
my $node_a_token = create_token_data( 0 ); # Fresh token
my $node_b_token = create_token_data( 0 ); # Another fresh token
# Both tokens created ~same time
my $time_diff = abs( $node_a_token->{ created_at } - $node_b_token->{ created_at } );
ok( $time_diff < 2, 'Concurrent tokens created within 2 seconds' );
# Race condition check: should skip write if another token exists within 5s
ok( $time_diff < 5, 'Falls within race condition mitigation window (5s)' );
}
done_testing();
print "\nToken Cache Tests Summary:\n";
print "=" x 50 . "\n";
print "All tests validate the token caching mechanism:\n";
print "- Token TTL validation (80% refresh threshold)\n";
print "- Cache file operations (read/write)\n";
print "- Race condition mitigation\n";
print "- Concurrent token handling\n";
print "- Atomic write operations\n";
print "=" x 50 . "\n";
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More tests => 9;
use File::Temp qw(tempdir);
use File::Path qw(make_path);
# Mock the command paths for testing
my $test_dir = tempdir( CLEANUP => 1 );
my $cmd = {
multipath => "$test_dir/multipath",
multipathd => "$test_dir/multipathd",
blockdev => "$test_dir/blockdev",
dmsetup => "$test_dir/dmsetup",
kpartx => "$test_dir/kpartx"
};
# Track if commands were checked
my $commands_checked = 0;
sub ensure_commands_checked {
return if $commands_checked;
check_commands();
$commands_checked = 1;
}
sub get_command_path {
my ( $name ) = @_;
ensure_commands_checked();
my $path = $cmd->{ $name };
if ( !defined $path ) {
die "Error :: Unknown command '$name'\n";
}
if ( !-x $path ) {
die "Error :: Command '$name' not found or not executable at '$path'\n";
}
return $path;
}
sub check_commands {
my @missing;
foreach my $name ( keys %$cmd ) {
my $path = $cmd->{ $name };
if ( !-x $path ) {
push @missing, "$name ($path)";
}
}
if ( @missing ) {
note "Warning :: The following commands are not available:";
note " - $_" foreach @missing;
}
return scalar @missing == 0;
}
# Test 1: Unknown command should die
eval { get_command_path('unknown_command') };
like( $@, qr/Unknown command/, 'Unknown command throws error' );
# Test 2: Non-existent command should die
eval { get_command_path('multipath') };
like( $@, qr/not found or not executable/, 'Non-existent command throws error' );
# Test 3: Create executable commands
foreach my $name ( keys %$cmd ) {
my $path = $cmd->{ $name };
open my $fh, '>', $path or die "Cannot create $path: $!";
print $fh "#!/bin/sh\necho 'test'\n";
close $fh;
chmod 0755, $path;
}
# Reset check flag to re-run validation
$commands_checked = 0;
# Test 4: Valid command should return path
my $path = get_command_path('multipath');
is( $path, $cmd->{multipath}, 'Valid command returns correct path' );
# Test 5: Commands should be checked only once
my $check_count = 0;
{
no warnings 'redefine';
my $original = \&check_commands;
*check_commands = sub {
$check_count++;
$original->();
};
$commands_checked = 0;
get_command_path('multipath');
get_command_path('dmsetup');
get_command_path('kpartx');
}
is( $check_count, 1, 'check_commands called only once' );
# Test 6: All commands should be validated
my $result = check_commands();
ok( $result, 'All commands are valid' );
# Test 7: Make one command non-executable
chmod 0644, $cmd->{kpartx};
$result = check_commands();
ok( !$result, 'Non-executable command detected' );
# Test 8: Check that non-executable command fails
eval { get_command_path('kpartx') };
like( $@, qr/not executable/, 'Non-executable command throws error' );
# Test 9: Restore executable and verify it works
chmod 0755, $cmd->{kpartx};
$commands_checked = 0;
$path = get_command_path('kpartx');
is( $path, $cmd->{kpartx}, 'Restored command works' );
# Test 10: Verify all expected commands exist in hash
my @expected = qw(multipath multipathd blockdev dmsetup kpartx);
my @actual = sort keys %$cmd;
is_deeply( \@actual, [sort @expected], 'All expected commands present in hash' );
done_testing();
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More tests => 8;
# Mock HTTP response
package MockHTTPResponse {
sub new {
my ( $class, $code, $success ) = @_;
return bless { code => $code, success => $success }, $class;
}
sub code { $_[0]->{code} }
sub is_success { $_[0]->{success} }
}
# Test retry counter logic
my $retry_count = 0;
my $max_retries = 1;
my @responses;
sub simulate_request {
my $response = shift @responses;
return $response;
}
# Test 1: Success on first try (no retry)
@responses = ( MockHTTPResponse->new(200, 1) );
$retry_count = 0;
while ( $retry_count <= $max_retries ) {
my $response = simulate_request();
if ( $response->code == 401 ) {
$retry_count++;
if ( $retry_count <= $max_retries ) {
next; # Retry
}
}
last; # Success or max retries
}
is( $retry_count, 0, 'No retry on successful response' );
# Test 2: One retry on 401, then success
@responses = (
MockHTTPResponse->new(401, 0),
MockHTTPResponse->new(200, 1)
);
$retry_count = 0;
while ( $retry_count <= $max_retries ) {
my $response = simulate_request();
if ( $response->code == 401 ) {
$retry_count++;
if ( $retry_count <= $max_retries ) {
next;
}
}
last;
}
is( $retry_count, 1, 'One retry on 401 response' );
# Test 3: Max retries reached (401 twice)
@responses = (
MockHTTPResponse->new(401, 0),
MockHTTPResponse->new(401, 0)
);
$retry_count = 0;
while ( $retry_count <= $max_retries ) {
my $response = simulate_request();
if ( $response->code == 401 ) {
$retry_count++;
if ( $retry_count <= $max_retries ) {
next;
} else {
last; # Max retries
}
}
last;
}
is( $retry_count, 2, 'Max retries (2) attempted on repeated 401' );
# Test 4: Loop exits after max retries
ok( $retry_count > $max_retries, 'Retry count exceeds max_retries after exhausting' );
# Test 5: No retry on non-401 errors
@responses = ( MockHTTPResponse->new(500, 0) );
$retry_count = 0;
while ( $retry_count <= $max_retries ) {
my $response = simulate_request();
if ( $response->code == 401 ) {
$retry_count++;
if ( $retry_count <= $max_retries ) {
next;
}
}
last;
}
is( $retry_count, 0, 'No retry on 500 error' );
# Test 6: Retry counter increments correctly
my @counts;
@responses = (
MockHTTPResponse->new(401, 0),
MockHTTPResponse->new(401, 0),
MockHTTPResponse->new(200, 1)
);
$retry_count = 0;
while ( $retry_count <= $max_retries ) {
push @counts, $retry_count;
my $response = simulate_request();
if ( $response->code == 401 ) {
$retry_count++;
if ( $retry_count <= $max_retries ) {
next;
}
}
last;
}
is_deeply( \@counts, [0, 1], 'Retry counter increments: 0, 1' );
# Test 7: Max retries = 1 allows exactly 1 retry
is( $max_retries, 1, 'Max retries configured to 1' );
# Test 8: Total attempts = max_retries + 1
my $total_attempts = $max_retries + 1;
is( $total_attempts, 2, 'Total attempts = 2 (initial + 1 retry)' );
done_testing();
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More tests => 16;
use File::Temp qw(tempdir);
use File::Path qw(make_path remove_tree);
use JSON;
# Create temporary cache directory
my $cache_dir = tempdir( CLEANUP => 1 );
# Mock functions from plugin
sub get_token_cache_path {
my ( $storeid, $array_index ) = @_;
my $dir = "$cache_dir/purestorage";
make_path($dir) unless -d $dir;
chmod 0700, $dir;
return "$dir/${storeid}_array${array_index}.json";
}
sub write_token_cache {
my ( $cache_path, $token_data ) = @_;
my $temp_path = "$cache_path.tmp.$$";
open my $fh, '>', $temp_path or die "Cannot write to $temp_path: $!";
print $fh encode_json($token_data);
close $fh;
chmod 0600, $temp_path;
rename $temp_path, $cache_path or die "Cannot rename $temp_path to $cache_path: $!";
}
sub read_token_cache {
my ( $cache_path ) = @_;
return undef unless -f $cache_path;
open my $fh, '<', $cache_path or return undef;
my $content = do { local $/; <$fh> };
close $fh;
return undef unless $content;
my $data = eval { decode_json($content) };
return undef if $@;
return $data;
}
sub is_token_valid {
my ( $token_data, $ttl ) = @_;
return 0 unless defined $token_data;
return 0 unless defined $token_data->{auth_token};
return 0 unless defined $token_data->{created_at};
my $now = time();
my $age = $now - $token_data->{created_at};
my $refresh_threshold = $ttl * 0.8;
return $age < $refresh_threshold;
}
sub cleanup_expired_cache {
my ( $cache_path, $ttl ) = @_;
my $token_data = read_token_cache($cache_path);
return unless $token_data;
my $now = time();
if ( $now > $token_data->{expires_at} ) {
unlink $cache_path;
}
}
# Test 1: Cache directory creation
my $cache_path = get_token_cache_path('pure', 0);
ok( -d "$cache_dir/purestorage", 'Cache directory created' );
# Test 2: Cache directory permissions
my $mode = (stat("$cache_dir/purestorage"))[2] & 0777;
is( $mode, 0700, 'Cache directory has correct permissions (700)' );
# Test 3: Write token cache
my $token_data = {
auth_token => 'test-token-12345',
request_id => 'req-67890',
created_at => time(),
ttl => 3600,
expires_at => time() + 3600
};
write_token_cache($cache_path, $token_data);
ok( -f $cache_path, 'Token cache file created' );
# Test 4: Cache file permissions
$mode = (stat($cache_path))[2] & 0777;
is( $mode, 0600, 'Cache file has correct permissions (600)' );
# Test 5: Read token cache
my $read_data = read_token_cache($cache_path);
ok( defined $read_data, 'Token cache read successfully' );
# Test 6: Verify token data
is( $read_data->{auth_token}, 'test-token-12345', 'Auth token matches' );
is( $read_data->{request_id}, 'req-67890', 'Request ID matches' );
# Test 7: Valid token (fresh)
ok( is_token_valid($read_data, 3600), 'Fresh token is valid' );
# Test 8: Valid token at 79% of TTL
$token_data->{created_at} = time() - (3600 * 0.79);
write_token_cache($cache_path, $token_data);
$read_data = read_token_cache($cache_path);
ok( is_token_valid($read_data, 3600), 'Token at 79% TTL is still valid' );
# Test 9: Invalid token at 81% of TTL
$token_data->{created_at} = time() - (3600 * 0.81);
write_token_cache($cache_path, $token_data);
$read_data = read_token_cache($cache_path);
ok( !is_token_valid($read_data, 3600), 'Token at 81% TTL is invalid' );
# Test 10: Multiple array caches
my $cache_path_1 = get_token_cache_path('pure', 1);
write_token_cache($cache_path_1, $token_data);
ok( -f $cache_path_1, 'Second array cache created' );
isnt( $cache_path, $cache_path_1, 'Different cache files for different arrays' );
# Test 11: Cleanup expired cache
$token_data->{created_at} = time() - 4000;
$token_data->{expires_at} = time() - 400; # Expired
write_token_cache($cache_path, $token_data);
cleanup_expired_cache($cache_path, 3600);
ok( !-f $cache_path, 'Expired cache file removed' );
# Test 12: Read non-existent cache
my $missing_cache = read_token_cache("$cache_dir/nonexistent.json");
is( $missing_cache, undef, 'Non-existent cache returns undef' );
# Test 13: Invalid JSON in cache
my $corrupt_cache = "$cache_dir/purestorage/corrupt.json";
open my $fh, '>', $corrupt_cache;
print $fh "{ invalid json }";
close $fh;
my $corrupt_data = read_token_cache($corrupt_cache);
is( $corrupt_data, undef, 'Corrupt cache returns undef' );
# Test 14: Missing required fields
my $incomplete_data = { auth_token => 'test' }; # Missing created_at
ok( !is_token_valid($incomplete_data, 3600), 'Incomplete token data is invalid' );
done_testing();