mirror of
https://github.com/kolesa-team/pve-purestorage-plugin.git
synced 2026-08-12 21:53:06 -06:00
f9f8eed94b
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
154 lines
4.4 KiB
Perl
154 lines
4.4 KiB
Perl
#!/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();
|