mirror of
https://github.com/kolesa-team/pve-purestorage-plugin.git
synced 2026-08-12 21:53:06 -06:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c4411ede9 | |||
| 45bb0e24eb | |||
| 2c222ef704 | |||
| 40eb92df32 | |||
| f7d06166fc | |||
| ed358b13c5 | |||
| 397e2f5b64 | |||
| d6b0035d5e | |||
| e0e4009426 | |||
| c1ff0acf15 | |||
| 63b2c46104 | |||
| 0ace4fe87d | |||
| c666235082 | |||
| 1c719ba7f8 | |||
| f9f8eed94b | |||
| c693d189df | |||
| a488acbe59 | |||
| f9a458efae | |||
| 66fa249d91 | |||
| b03b80a7b4 | |||
| 14096218b9 | |||
| 422063e49a | |||
| 71c7074675 | |||
| 098ea4dc4f | |||
| 0aebefe4d3 | |||
| e0fcb81e69 | |||
| b4c81dd5a2 | |||
| 08955ca5e1 | |||
| 6d70cb8c4b | |||
| aac760b3cb | |||
| aeb3547d5e | |||
| 87b579b65a | |||
| b016137857 | |||
| c029d3b90a | |||
| f1bf737d39 | |||
| 14d93fab80 | |||
| 7c77b06585 | |||
| 58265255f1 | |||
| d93f562bdc | |||
| a32f42999f | |||
| 32ed77d155 | |||
| 44e53e7e20 | |||
| e6abf236a0 | |||
| 7bb26c2a7b | |||
| 7b5b78ad55 | |||
| 576a4d4181 | |||
| 4f3f2d081d | |||
| 3cb2f31699 | |||
| e7f429da28 | |||
| ac971e0425 | |||
| 06410fb99a | |||
| c156355589 | |||
| c9d4c07cde | |||
| 2f5bc26369 | |||
| 46ccf11ece | |||
| 7bc3922674 | |||
| d01feb85a8 | |||
| 75625bdbdb | |||
| fbd1493369 | |||
| 236ab82740 | |||
| ae51b4b510 | |||
| 1eeec41f40 | |||
| 6ca954e50f | |||
| 25aba73428 | |||
| c249fe46cb | |||
| f248f00e14 | |||
| 424b94b2a9 | |||
| 6b37e1ea81 | |||
| a0fcb1437c | |||
| 752b6fe06f | |||
| 71542b1b94 | |||
| 725e99023f | |||
| 4a1bec377f |
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"MD013": {
|
||||||
|
"tables": false,
|
||||||
|
"code_blocks": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
# .perltidyrc - Configuration for perltidy
|
# .perltidyrc - Configuration for perltidy
|
||||||
|
|
||||||
# Indentation
|
## Indentation
|
||||||
|
|
||||||
--indent-columns=2
|
--indent-columns=2
|
||||||
--continuation-indentation=2
|
--continuation-indentation=2
|
||||||
--extended-continuation-indentation
|
--extended-continuation-indentation
|
||||||
|
|
||||||
# Line length and wrapping
|
## Line length and wrapping
|
||||||
|
|
||||||
--maximum-line-length=160
|
--maximum-line-length=160
|
||||||
|
|
||||||
# Brackets spaces
|
## Brackets spaces
|
||||||
|
|
||||||
--paren-tightness=0
|
--paren-tightness=0
|
||||||
--brace-tightness=0
|
--brace-tightness=0
|
||||||
--block-brace-tightness=0
|
--block-brace-tightness=0
|
||||||
|
|
||||||
# Comments
|
## Comments
|
||||||
|
|
||||||
|
## Blocks
|
||||||
|
|
||||||
# Blocks
|
|
||||||
--cuddled-else
|
--cuddled-else
|
||||||
--cuddled-blocks
|
--cuddled-blocks
|
||||||
+1527
-615
File diff suppressed because it is too large
Load Diff
@@ -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.
|
[](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
|
||||||
@@ -56,51 +106,161 @@ blacklist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
blacklist_exceptions {
|
blacklist_exceptions {
|
||||||
wwid "624a9370.*"
|
wwid "3624a9370.*"
|
||||||
device {
|
device {
|
||||||
vendor "PURE"
|
vendor "PURE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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 manually edit the storage configuration file `/etc/pve/storage.cfg`.
|
> [!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
|
||||||
|
pvesm add purestorage <storage_id> \
|
||||||
|
--nodes <proxmox_node_list> \
|
||||||
|
--address \
|
||||||
|
https://<purestorage_fqdn_or_ip> \
|
||||||
|
--token <purestorage_api_token> \
|
||||||
|
--vgname <purestorage_volume_group_name> \
|
||||||
|
--hgsuffix <purestorage_host_suffix>
|
||||||
|
--content images
|
||||||
```
|
```
|
||||||
purestorage: pure
|
|
||||||
nodes: <proxmox_node_list>
|
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>
|
||||||
|
nodes <proxmox_node_list>
|
||||||
address https://<purestorage_fqdn_or_ip>
|
address https://<purestorage_fqdn_or_ip>
|
||||||
token <purestorage_api_token>
|
token <purestorage_api_token>
|
||||||
vgname <purestorage_volume_group_name>
|
vgname <purestorage_volume_group_name>
|
||||||
@@ -110,59 +270,394 @@ purestorage: pure
|
|||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
| --------- | ----------- |
|
| --------- | ----------- |
|
||||||
|
| 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 | The name of the volume group where new virtual disks will be created. 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. |
|
||||||
|
| 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.
|
||||||
|
|||||||
Vendored
+8
-1
@@ -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+.
|
||||||
|
|||||||
Executable
+95
@@ -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"
|
||||||
@@ -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
|
||||||
Executable
+67
@@ -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
|
||||||
Executable
+205
@@ -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";
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user