commit c252c0310fdae965e67ceeb780cf6ea2a3ddd5d1 Author: timansky Date: Wed Dec 4 22:27:16 2024 +0500 Initial commit diff --git a/.github/workflows/_deb.yml b/.github/workflows/_deb.yml new file mode 100644 index 0000000..7df20cc --- /dev/null +++ b/.github/workflows/_deb.yml @@ -0,0 +1,73 @@ +name: Build deb + +concurrency: + group: branch-${{ github.ref_name }} + cancel-in-progress: true + +on: + workflow_call: + outputs: + deb: + description: "Deb artifact url FQIN" + value: ${{ jobs.config.outputs.fqin_release }} + +jobs: + build-deb: + name: Build deb + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + path: src + + - name: Install depedencies + working-directory: ./src + run: | + sudo apt-get build-dep . + + - name: Generate changelog + working-directory: ./src + run: | + pkgname=libpve-storage-purestorage-perl + tag_list=$(git tag -l | grep -ve '-rc\.[0-9]$' | grep -ve '-beta\.[0-9]$' | sort -V) + for tag in ${tag_list}; do + tag_header="$tag^..$tag" + tag_info=$prevtag..$tag + tag_version=$(echo $tag | cut -c2-)-1 + if [[ "$prevrag" == "" ]]; 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 + + - name: Build deb + working-directory: ./src + run: | + dpkg-buildpackage -us -uc + + - name: generate sha256sums + run: | + sha256sum *.deb > sha256sums + + - id: artifact-upload + uses: actions/upload-artifact@v4 + with: + name: deb-artifact-${{ github.sha }} + if-no-files-found: error + compression-level: 0 + overwrite: true + path: | + *.deb + sha256sums diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..123eb8c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release +run-name: Release-${{ github.ref_name }}-${{ github.run_number }} + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +on: + workflow_dispatch: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]' + - 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]' + +jobs: + test: + name: Test release tag + runs-on: ubuntu-22.04 + steps: + - run: | + echo ${{ github.ref }} + - if: ${{ github.ref_type != 'tag' }} + run: | + echo "::error::${{ github.ref }} branch is not a release tag"; exit 1 + + build: + name: Make deb package + needs: + - test + uses: ./.github/workflows/_deb.yml + secrets: inherit + + release: + name: Release + needs: + - build + runs-on: ubuntu-22.04 + steps: + - id: artifact-download + uses: actions/download-artifact@v4 + with: + name: deb-artifact-${{ github.sha }} + + - uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + generate_release_notes: true + make_latest: true + prerelease: true + files: | + *.deb + sha256sums diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0bd0f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +debian/.debhelper +debian/changelog +debian/debhelper-build-stamp +debian/libpve-storage-purestorage-perl +debian/files +debian/package +debian/tmp +/*.buildinfo +/*.deb +*.log +*.substvars diff --git a/.pertidyrc b/.pertidyrc new file mode 100644 index 0000000..cdfb8c2 --- /dev/null +++ b/.pertidyrc @@ -0,0 +1,20 @@ +# .perltidyrc - Configuration for perltidy + +# Indentation +--indent-columns=2 +--continuation-indentation=2 +--extended-continuation-indentation + +# Line length and wrapping +--maximum-line-length=160 + +# Brackets spaces +--paren-tightness=0 +--brace-tightness=0 +--block-brace-tightness=0 + +# Comments + +# Blocks +--cuddled-else +--cuddled-blocks diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..1928dfa --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Timur Kumakbayev \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..ebe02fc --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @timansky @zigmund diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..453c43b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +The following is a set of guidelines for contributing to nginx project. +We really appreciate that you are considering contributing! + +## Table of Contents + +- [Ask a Question](#ask-a-question) +- [Report a Bug](#report-a-bug) +- [Suggest a Feature or Enhancement](#suggest-a-feature-or-enhancement) +- [Open a Discussion](#open-a-discussion) +- [Submit a Pull Request](#submit-a-pull-request) +- [Issue Lifecycle](#issue-lifecycle) + +## Ask a Question + +To ask a question, open an issue on GitHub with the label `question`. + +## Report a Bug + +To report a bug, open an issue on GitHub with the label `bug` using the +available bug report issue template. Before reporting a bug, make sure the +issue has not already been reported. + +## Suggest a Feature or Enhancement + +To suggest a feature or enhancement, open an issue on GitHub with the label +`feature` or `enhancement` using the available feature request issue template. +Please ensure the feature or enhancement has not already been suggested. + + +## Submit a Pull Request + +Follow this plan to contribute a change to plugin source code: + +- Fork repository +- Create a branch +- Implement your changes in this branch +- Submit a pull request (PR) when your changes are tested and ready for review + +### Formatting Changes + +- Changes should be formatted according to the code style + +- Keep a clean, concise and meaningful commit history on your branch, rebasing +locally and breaking changes logically into commits before submitting a PR + +- Each commit message should have a single-line subject line followed by verbose +description after an empty line + +- Limit the subject line to 67 characters, and the rest of the commit message +to 76 characters + +- Reference issues in the the subject line; if the commit fixes an issue, +[name it](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) +accordingly + +### Before Submitting + +- Try to make it clear why the suggested change is needed, and provide a use +case, if possible diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2faa835 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Kolesa, JSC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PureStoragePlugin.pm b/PureStoragePlugin.pm new file mode 100644 index 0000000..6b5fff3 --- /dev/null +++ b/PureStoragePlugin.pm @@ -0,0 +1,1044 @@ +package PVE::Storage::Custom::PureStoragePlugin; + +use strict; +use warnings; + +use Data::Dumper qw( Dumper ); # DEBUG + +use IO::File (); +use Net::IP (); +use File::Path (); + +use PVE::JSONSchema (); +use PVE::Network (); +use PVE::Tools qw( run_command ); +use PVE::INotify (); +use PVE::Storage::Plugin (); + +use JSON::XS qw( decode_json encode_json ); +use LWP::UserAgent (); +use HTTP::Headers (); +use HTTP::Request (); +use URI::Escape qw( uri_escape ); +use File::Basename qw( basename ); + +use base qw(PVE::Storage::Plugin); + +push @PVE::Storage::Plugin::SHARED_STORAGE, 'purestorage'; +$Data::Dumper::Terse = 1; # Removes `$VAR1 =` in output +$Data::Dumper::Indent = 1; # Outputs everything in one line +$Data::Dumper::Useqq = 1; # Uses quotes for strings + +my $purestorage_wwn_prefix = "624a9370"; + +my $DEBUG = 0; + +### BLOCK: Asserts + +my $cmd = {}; # Initialize as a hash, not an array +$cmd->{ "iscsiadm" } = "/usr/bin/iscsiadm"; +my $found_iscsi_adm_support; + +sub assert_iscsi_support { + my ( $class, $noerr ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::assert_iscsi_support\n" if $DEBUG; + return $found_iscsi_adm_support if $found_iscsi_adm_support; # assume it won't be removed if ever found + + my $found_iscsi_adm_exe = -x $cmd->{ "iscsiadm" }; + + if ( $found_iscsi_adm_exe ) { + return 1; + } + die "Error :: no iSCSI support - please install open-iscsi.\n" if !$noerr; + warn "Warning :: no iSCSI support - please install open-iscsi.\n"; + return 0; +} + +$cmd->{ "multipath" } = "/sbin/multipath"; +$cmd->{ "multipathd" } = "/sbin/multipath"; +my $found_multipath_support; + +sub assert_multipath_support { + my ( $class, $noerr ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::assert_multipath_support\n" if $DEBUG; + return $found_multipath_support if $found_multipath_support; # assume it won't be removed if ever found + $class->assert_iscsi_support(); + + my $found_multipath_exe = -x $cmd->{ "multipath" }; + my $found_multipathd_exe = -x $cmd->{ "multipathd" }; + + if ( $found_multipath_exe && $found_multipathd_exe ) { + return 1; + } + die "Error :: no multipath support - please install multipath-tools.\n" if !$noerr; + warn "Warning :: no multipath support - please install multipath-tools.\n"; + return 0; +} + +$cmd->{ "blockdev" } = "/usr/sbin/blockdev"; +my $found_blockdev_support; + +sub assert_blockdev_support { + my ( $class, $noerr ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::assert_blockdev_support\n" if $DEBUG; + return $found_blockdev_support if $found_blockdev_support; # assume it won't be removed if ever found + + my $found_blockdev_exe = -x $cmd->{ "blockdev" }; + + if ( $found_blockdev_exe ) { + return 1; + } + die "Error :: no blockdev support - please install blockdev.\n" if !$noerr; + warn "Warning :: no blockdev support - please install blockdev.\n"; + return 0; +} + +### BLOCK: Configuration +sub api { + +# PVE 5: APIVER 2 +# PVE 6: APIVER 3 +# PVE 6: APIVER 4 e6f4eed43581de9b9706cc2263c9631ea2abfc1a / volume_has_feature +# PVE 6: APIVER 5 a97d3ee49f21a61d3df10d196140c95dde45ec27 / allow rename +# PVE 6: APIVER 6 8f26b3910d7e5149bfa495c3df9c44242af989d5 / prune_backups (fine, we don't support that content type) +# PVE 6: APIVER 7 2c036838ed1747dabee1d2c79621c7d398d24c50 / volume_snapshot_needs_fsfreeze (guess we are fine, upstream only implemented it for RDBPlugin; we are not that different to let's say LVM in this regard) +# PVE 6: APIVER 8 343ca2570c3972f0fa1086b020bc9ab731f27b11 / prune_backups (fine again, see APIVER 6) +# PVE 7: APIVER 9 3cc29a0487b5c11592bf8b16e96134b5cb613237 / resets APIAGE! changes volume_import/volume_import_formats +# PVE 7.1: APIVER 10 a799f7529b9c4430fee13e5b939fe3723b650766 / rm/add volume_snapshot_{list,info} (not used); blockers to volume_rollback_is_possible (not used) + + my $apiver = 10; + + return $apiver; +} + +sub type { + return "purestorage"; +} + +sub plugindata { + return { + content => [ { images => 1, none => 1 }, { images => 1 } ], + format => [ { raw => 1 }, "raw" ], + }; +} + +sub properties { + return { + hgsuffix => { + description => "Host group suffx.", + type => "string", + default => "pve" + }, + address => { + description => "PureStorage Management IP address or DNS name.", + type => "string" + }, + token => { + description => "Storage API token.", + type => "string" + }, + check_ssl => { + description => "Verify the server's TLS certificate", + type => "boolean", + default => "no" + }, + }; +} + +sub options { + return { + address => { fixed => 1 }, + token => { fixed => 1 }, + + # hgname => { fixed => 1 }, + hgsuffix => { fixed => 1 }, + vgname => { fixed => 1 }, + check_ssl => { optional => 1 }, + nodes => { optional => 1 }, + disable => { optional => 1 }, + content => { optional => 1 }, + format => { optional => 1 }, + }; +} + +### BLOCK: Local multipath => PVE::Storage::Custom::PureStoragePlugin::sub::s + +sub purestorage_request { + my ( $class, $scfg, $type, $method, $params, $body ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_request\n" if $DEBUG; + + my $api = "2.26"; + my $url = $scfg->{ address }; + my $check_ssl = $scfg->{ check_ssl } ? 1 : 0; + + $url .= "/api/$api/$type"; + $url .= "?$params" if $params; + + my $token = + $type eq "login" + ? $scfg->{ token } + : $class->purestorage_get_auth_token( $scfg ); + my $headers = HTTP::Headers->new( + ( $type eq "login" ? "api-token" : "x-auth-token" ) => $token, + "Content-Type" => "application/json" + ); + my $ua = LWP::UserAgent->new; + $ua->ssl_opts( + verify_hostname => 0, + SSL_verify_mode => 0x00 + ) if !$check_ssl; + my $request = HTTP::Request->new( $method, $url, $headers, $body ? encode_json( $body ) : undef ); + my $response = $ua->request( $request ); + my $content_type = $response->header( "Content-Type" ); + my $content = + defined $content_type && $content_type =~ /application\/json/ && $response->content ne "" + ? decode_json( $response->content ) + : $response->decoded_content; + + return { + content => $content, + headers => $response->headers, + error => $response->is_success ? undef : $response->code, + }; +} + +sub purestorage_get_auth_token { + my ( $class, $scfg ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_get_auth_token\n" if $DEBUG; + + my $response = $class->purestorage_request( $scfg, "login", "POST" ); + + if ( $response->{ error } ) { + die "Error :: PureStorage API :: Authentication failed.\n" . "=> Trace:\n" . "==> Code: " . $response->{ error } . "\n" . $response->{ content } + ? "==> Message: " . Dumper( $response->{ content } ) + : ""; + } + + my $auth_token = $response->{ headers }->header( "x-auth-token" ) || die "Header 'x-auth-token' missing."; + + return $auth_token; +} + +sub purestorage_volume_info { + my ( $class, $scfg, $volname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_volume_info\n" if $DEBUG; + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + + my $filter = "name='$vgname/$volname'"; + my $response = $class->purestorage_request( $scfg, "volumes", "GET", "filter=" . uri_escape( $filter ) ); + + if ( $response->{ error } ) { + die "Error :: PureStorage API :: Get volume '$volname' info failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + + my $volumes = $response->{ content }->{ items }; + unless ( ref( $volumes ) eq 'ARRAY' && @$volumes ) { + die "Error :: PureStorage API :: No volume data found for '$volname'.\n"; + } + + my $volume = $volumes->[0]; + my $size = $volume->{ provisioned } || 0; + my $used = $volume->{ space }->{ total_used } || 0; + + print "Debug :: Provisioned: $size, Used: $used\n" if $DEBUG; + + return ( $size, $used ); +} + +sub purestorage_list_volumes { + my ( $class, $scfg, $vmid, $storeid, $destroyed ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_list_volumes\n" if $DEBUG; + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + $class->assert_multipath_support(); + + my $filter; + if ( defined( $vmid ) ) { + $filter = "name='$vgname/vm-$vmid-disk-*'"; + $filter .= " or name='$vgname/vm-$vmid-cloudinit'"; + } else { + $filter = "name='$vgname/*'"; + } + + if ( defined( $destroyed ) ) { + $filter .= $destroyed ? " and destroyed='true'" : " and destroyed='false'"; + } + + my $response = $class->purestorage_request( $scfg, "volumes", "GET", "filter=" . uri_escape( $filter ) ); + + if ( $response->{ error } ) { + die "Error :: PureStorage API :: List volumes status failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + + my @volumes = map { + my $volname = $_->{ name }; + + $volname =~ s/^$scfg->{vgname}\///; + + my ( undef, undef, $volvm ) = $class->parse_volname( $volname ); + + my $ctime = int( $_->{ created } / 1000 ); + { + name => $volname, + vmid => $volvm, + serial => $_->{ serial }, + size => $_->{ provisioned }, + ctime => $ctime, + volid => $storeid ? "$storeid:$volname" : $volname, + format => "raw" + } + } @{ $response->{ content }->{ items } }; + + return \@volumes; +} + +sub purestorage_get_wwn { + my ( $class, $scfg, $volname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_get_wwn\n" if $DEBUG; + + my ( $vtype, $name, $vmid ) = $class->parse_volname( $volname ); + my $volumes = $class->purestorage_list_volumes( $scfg, $vmid ); + + foreach my $volume ( @$volumes ) { + if ( $volume->{ name } =~ /vm-$vmid-$name/ ) { + + # Construct the WWN path + my $path = lc( "/dev/disk/by-id/wwn-0x" . $purestorage_wwn_prefix . $volume->{ serial } ); + my $wwn = lc( "3" . $purestorage_wwn_prefix . $volume->{ serial } ); + return ( $path, $vmid, $vtype, $wwn ); + } + } + + return 0; +} + +sub purestorage_unmap_disk { + my ( $class, $disk_name ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_unmap_disk\n" if $DEBUG; + + if ( $disk_name =~ m|^(sd[a-z]+)$| ) { + $disk_name = $1; # untaint; + my $sysfs_path = "/sys/block/$disk_name"; + my $disk_path = "/dev/$disk_name"; + + if ( -e $disk_path ) { + run_command( [ $cmd->{ "blockdev" }, "--flushbufs", $disk_path ] ); + } + + my $fh; + open( $fh, ">", $sysfs_path . "/device/state" ) or die "Could not open file '$sysfs_path/device/state' for writing.\n"; + print $fh "offline"; + close( $fh ); + + open( $fh, ">", $sysfs_path . "/device/delete" ) or die "Could not open file '$sysfs_path/device/delete' for writing.\n"; + print $fh "1"; + close( $fh ); + } + return 1; +} + +sub purestorage_rescan_diskmap { + my ( $class, $path ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_rescan_diskmap\n" if $DEBUG; + + eval { run_command( [ $cmd->{ "iscsiadm" }, "--mode", "node", "--rescan" ] ) }; + eval { run_command( [ $cmd->{ "iscsiadm" }, "--mode", "session", "--rescan" ] ) }; + eval { run_command( [ $cmd->{ "multipath" }, "-W" ] ) }; + eval { run_command( [ $cmd->{ "multipath" }, "-r", $path ] ) }; + sleep 1; + + return 1; +} + +sub purestorage_cleanup_diskmap { + my ( $class ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_cleanup_diskmap\n" if $DEBUG; + + my @disks = `lsblk -o NAME,TYPE,SIZE -nr`; + + foreach my $disk_name ( @disks ) { + my ( $name, $type, $size ) = split( /\s+/, $disk_name ); + + if ( $type eq 'disk' && $size eq '0B' ) { + $class->purestorage_unmap_disk( $name ); + } + } + + return 1; +} + +sub purestorage_volume_connection { + my ( $class, $scfg, $volname, $action ) = @_; + + $action //= 'POST'; + + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_volume_connection :: $action\n" if $DEBUG; + + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + my $url = $scfg->{ address } || die "Error: Pure Storage host is not defined.\n"; + my $hname = PVE::INotify::nodename() . "-" . $scfg->{ hgsuffix }; + + my $params = "host_names=$hname&volume_names=$vgname/$volname"; + my $response = $class->purestorage_request( $scfg, "connections", $action, $params ); + + if ( $response->{ error } ) { + if ( $response->{ content }->{ errors }->[0]->{ message } eq "Connection already exists." ) { + warn "Error :: PureStorage API :: Connections '$volname' to '$hname' already exist.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } elsif ( $response->{ content }->{ errors }->[0]->{ message } eq "Volume has been destroyed." ) { + warn "Error :: PureStorage API :: Failed to modify connection :: Nothing to remove.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } elsif ( $response->{ content }->{ errors }->[0]->{ message } eq "Connection does not exist." ) { + warn "Error :: PureStorage API :: Failed to modify connection :: Nothing to remove.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } else { + $Data::Dumper::Indent = 0; + die "Error :: PureStorage API :: Failed to modify connection.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + } + + if ( $action eq "DELETE" ) { + print "Volume '$volname' successfully removed from host '$hname'.\n"; + } + print "Volume '$volname' successfully added to host '$hname'.\n"; + return 1; +} + +sub purestorage_create_volume { + my ( $class, $scfg, $volname, $size, $storeid ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_create_volume\n" if $DEBUG; + + $class->assert_multipath_support(); + + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + my $url = $scfg->{ address } || die "Error: Pure Storage host is not defined.\n"; + + my $hname = PVE::INotify::nodename() . "-" . $scfg->{ hgsuffix }; + + my $params; + my $volparams; + my $serial; + my $response; + + print "Step: Create the volume.\n"; + $params = "names=$vgname/$volname"; + $volparams = { "provisioned" => $size }; + + $response = $class->purestorage_request( $scfg, "volumes", "POST", $params, $volparams ); + + if ( $response->{ error } ) { + die "Error :: PureStorage API :: Create volume failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + + $serial = $response->{ content }->{ items }->[0]->{ serial } || die "Error: Failed to retrieve volume serial"; + print "Volume '$volname' in Volume Group '$vgname' created successfully.\n"; + + return 1; +} + +sub purestorage_remove_volume { + my ( $class, $scfg, $volname, $storeid, $eradicate ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_remove_volume\n" if $DEBUG; + + $eradicate //= 0; + + $class->assert_blockdev_support(); + $class->assert_multipath_support(); + + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + my $url = $scfg->{ address } || die "Error: Pure Storage host is not defined.\n"; + + my $hname = PVE::INotify::nodename() . "-" . $scfg->{ hgsuffix }; + my ( undef, undef, $vmid ) = $class->parse_volname( $volname ); + + my $params; + my $response; + + $eradicate = ( $volname =~ /^vm-(\d+)-cloudinit/ ) ? 1 : $eradicate; + + my $running = PVE::QemuServer::check_running( $vmid ); + if ( $running ) { + print "Step: Deactivate volume '$volname'.\n"; + $class->deactivate_volume( $storeid, $scfg, $volname ); + } + + print "Step: Remove the storage volume '$volname'.\n"; + + $params = "names=$vgname/$volname"; + my $body = { destroyed => \1 }; + + $response = $class->purestorage_request( $scfg, "volumes", "PATCH", $params, $body ); + if ( $response->{ error } ) { + if ( $response->{ content }->{ errors }->[0]->{ message } eq "Volume has been destroyed." ) { + warn "Warning :: PureStorage API :: Destroy volume failed :: Nothing to remove.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } else { + $Data::Dumper::Indent = 0; + die "Error :: PureStorage API :: Destroy volume failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + } else { + print "Volume '$volname' from volume group '$vgname' destroyed successfully.\n"; + } + + if ( $eradicate ) { + print "Step: Eradicate the storage volume.\n"; + + $params = "names=$vgname/$volname"; + + $response = $class->purestorage_request( $scfg, "volumes", "DELETE", $params, $body ); + if ( $response->{ error } ) { + $Data::Dumper::Indent = 0; + die "Error :: PureStorage API :: Eradicate volume failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } else { + print "Volume '$volname' from volume group '$vgname' eradicated successfully.\n"; + } + } + + $class->purestorage_cleanup_diskmap(); + + return 1; +} + +sub purestorage_resize_volume { + my ( $class, $scfg, $volname, $size ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_resize_volume\n" if $DEBUG; + + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + my $url = $scfg->{ address } || die "Error: Pure Storage host is not defined.\n"; + my $hname = PVE::INotify::nodename() . "-" . $scfg->{ hgsuffix }; + my ( $path, undef, undef, $wwid ) = $class->filesystem_path( $scfg, $volname ); + my $params = "names=$vgname/$volname"; + my $volparams = { "provisioned" => $size }; + my $response = $class->purestorage_request( $scfg, "volumes", "PATCH", $params, $volparams ); + + if ( $response->{ error } ) { + die "Error :: PureStorage API :: Resize volume failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + + print "Volume '$volname' in volume group '$vgname' resized successfully.\n"; + $class->purestorage_rescan_diskmap( $wwid ); + + return 1; +} + +sub purestorage_rename_volume { + my ( $class, $scfg, $source_volname, $target_volname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_rename_volume\n" if $DEBUG; + + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + my $url = $scfg->{ address } || die "Error: Pure Storage host is not defined.\n"; + my $params = "names=$vgname/$source_volname"; + my $volparams = { "name" => "$vgname/$target_volname" }; + my $response = $class->purestorage_request( $scfg, "volumes", "PATCH", $params, $volparams ); + + if ( $response->{ error } ) { + die "Error :: PureStorage API :: Rename volume failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + + print "Volume '$source_volname' in volume group '$vgname' renamed to '$target_volname' successfully.\n"; + + return 1; +} + +sub purestorage_snap_volume_create { + my ( $class, $scfg, $snap_name, $volname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_snap_volume_create\n" if $DEBUG; + + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + my $params; + my $response; + + $params = "source_names=$vgname/$volname&suffix=snap-$snap_name"; + + $response = $class->purestorage_request( $scfg, "volume-snapshots", "POST", $params ); + + if ( $response->{ error } ) { + die "Error :: PureStorage API :: Snapshot volume failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + + print "Snapshot($snap_name) of Volume '$volname' in Volume Group $vgname successfully.\n"; + return 1; +} + +sub purestorage_snap_volume_rollback { + my ( $class, $scfg, $snap_name, $volname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_snap_volume_rollback\n" if $DEBUG; + + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + my $params; + my $response; + my $body; + + $params = "names=$vgname/$volname&overwrite=true"; + $body = { source => name => "$vgname/$volname.snap-$snap_name" }; + + $response = $class->purestorage_request( $scfg, "volumes", "PATCH", $params, $body ); + + if ( $response->{ error } ) { + die "Error :: PureStorage API :: Restore volume snapshot failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + + print "Snapshot($snap_name) of volume '$volname' in Volume Group '$vgname' restored succesfuly.\n"; + return 1; +} + +sub purestorage_snap_volume_delete { + my ( $class, $scfg, $snap_name, $volname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::purestorage_snap_volume_delete\n" if $DEBUG; + + my $vgname = $scfg->{ vgname } || die "Error: Volume group name is not defined.\n"; + my $params; + + my $response; + my $body; + + $params = "names=$vgname/$volname.snap-$snap_name"; + print $params; + $body = { destroyed => \1 }; + + $response = $class->purestorage_request( $scfg, "volume-snapshots", "PATCH", $params, $body ); + + if ( $response->{ error } ) { + my @valid_errors = + ( "Volume snapshot has been destroyed. It can be recovered by purevol recover and eradicated by purevol eradicate.", "No such volume or snapshot." ); + if ( grep { $_ eq $response->{ content }->{ errors }->[0]->{ message } } @valid_errors ) { + warn "Warning :: PureStorage API :: Destroy snapshot failed :: Nothing to destoy.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } else { + $Data::Dumper::Indent = 0; + die "Error :: PureStorage API :: Destroy volume snapshot failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + } + + print "Snapshot($snap_name) of volume '$volname' in Volume Group '$vgname' destroyed succesfuly.\n"; + return 1; +} + +### BLOCK: Storage implementation + +sub parse_volname { + my ( $class, $volname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::parse_volname\n" if $DEBUG; + + if ( $volname =~ m/^(vm|base)-(\d+)-(\S+)$/ ) { + my $vtype = ( $1 eq "vm" ) ? "images" : "base"; # Determine volume type + my $vmid = $2; # Extract VMID + my $name = $3; # Remaining part of the volume name + + return ( $vtype, $name, $vmid ); # Return type, name, and VMID + } + die "Error: Invalid volume name ($volname).\n"; + return 0; +} + +sub filesystem_path { + my ( $class, $scfg, $volname, $snapname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::filesystem_path\n" if $DEBUG; + + die "Error: snapshot is not implemented ($snapname).\n" if defined( $snapname ); + + my ( $path, $vmid, $vtype, $wwid ) = $class->purestorage_get_wwn( $scfg, $volname ); + + # print "Path: $path\n" if $DEBUG && $path; + # print "VMid: $vmid\n" if $DEBUG && $vmid; + # print "Vtype: $vtype\n" if $DEBUG && $vtype; + # print "WWN: $wwid\n" if $DEBUG && $wwid; + + if ( !defined( $path ) || !defined( $vmid ) || !defined( $vtype ) ) { + return wantarray ? ( "", "", "", "" ) : ""; + } + + return wantarray ? ( $path, $vmid, $vtype, $wwid ) : $path; +} + +sub create_base { + my ( $class, $storeid, $scfg, $volname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::create_base\n" if $DEBUG; + die "Error: Creating base image is currently unimplemented.\n"; +} + +sub clone_image { + my ( $class, $scfg, $storeid, $volname, $vmid, $snap ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::clone_image\n" if $DEBUG; + die "Error: Cloning image is currently unimplemented.\n"; +} + +sub find_free_diskname { + my ( $class, $storeid, $scfg, $vmid, $fmt, $add_fmt_suffix ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::find_free_diskname\n" if $DEBUG; + + my $disk_prefix = "$scfg->{vgname}/vm-$vmid-disk-"; + my $volumes = $class->purestorage_list_volumes( $scfg, $vmid, $storeid ); + my @disk_list = map { $_->{ name } } @$volumes; + + return PVE::Storage::Plugin::get_next_vm_diskname( \@disk_list, $storeid, $vmid, undef, $scfg ); +} + +sub alloc_image { + my ( $class, $storeid, $scfg, $vmid, $fmt, $name, $size ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::alloc_image\n" if $DEBUG; + + # Convert size from KB to bytes + my $sizeB = $size * 1024; # KB => B + + # Check for supported format (only 'raw' is allowed) + die "Error: Unsupported format ($fmt).\n" if $fmt ne 'raw'; + + # Validate the name format, should start with 'vm-$vmid-disk' + die "Error: Illegal name '$name' - should be 'vm-$vmid-(disk-*|cloudinit)'.\n" if $name && $name !~ m/^vm-$vmid-(disk-|cloudinit)/; + + $name = $class->find_free_diskname( $storeid, $scfg, $vmid ) if !$name; + + # Check size (must be between 1MB and 4PB) + die "Error: Invalid size '$size kb' < '1024 kb'.\n" unless $size > 1024; # Proxmox 1MB = 1049KB + + if ( !$class->purestorage_create_volume( $scfg, $name, $sizeB, $storeid ) ) { + warn "Error :: Failed to create volume '$name'"; + if ( !$class->purestorage_remove_volume( $scfg, $name, $storeid, 1 ) ) { + warn "Error :: Failed to destroy volume '$name'"; + } + die; + } + + return $name; +} + +sub free_image { + my ( $class, $storeid, $scfg, $volname, $isBase ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::free_image\n" if $DEBUG; + + $class->purestorage_remove_volume( $scfg, $volname, $storeid ); + + return undef; +} + +sub list_images { + my ( $class, $storeid, $scfg, $vmid, $vollist, $cache ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::list_images\n" if $DEBUG; + + $cache->{ purestorage } = $class->purestorage_list_volumes( $scfg, $vmid, $storeid, 0 ) if !$cache->{ purestorage }; + + return $cache->{ purestorage }; +} + +sub status { + my ( $class, $storeid, $scfg, $cache ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::status\n" if $DEBUG; + + # Send the request through the universal => PVE::Storage::Custom::PureStoragePlugin::sub:: + my $response = $class->purestorage_request( $scfg, "arrays/space", "GET" ); + + # Check if there was an error in the response + if ( $response->{ error } ) { + die "Error :: PureStorage API :: Get storage status failed.\n" + . "=> Trace:\n" + . "==> Code: " + . $response->{ error } . "\n" + . ( $response->{ content } ? "==> Message: " . Dumper( $response->{ content } ) : "" ); + } + + # Get storage capacity and used space from the response + my $total = $response->{ content }->{ items }->[0]->{ capacity }; + my $used = $response->{ content }->{ items }->[0]->{ space }->{ total_physical }; + + # my $used = $response->{ content }->{ items }->[0]->{ space }->{ total_used }; # Do not know what is correct + + # Calculate free space + my $free = $total - $used; + + # Mark storage as active + my $active = 1; + + # Return total, free, used space and the active status + return ( $total, $free, $used, $active ); +} + +sub activate_storage { + my ( $class, $storeid, $scfg, $cache ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::activate_storage\n" if $DEBUG; + $class->purestorage_cleanup_diskmap(); + + return 1; +} + +sub deactivate_storage { + my ( $class, $storeid, $scfg, $cache ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::deactivate_storage\n" if $DEBUG; + + return 1; +} + +sub volume_size_info { + my ( $class, $scfg, $storeid, $volname, $timeout ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::volume_size_info\n" if $DEBUG; + + my ( $size, $used ) = $class->purestorage_volume_info( $scfg, $volname ); + + return wantarray ? ( $size, "raw", $used, undef ) : $size; +} + +sub map_volume { + my ( $class, $storeid, $scfg, $volname, $snapname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::map_volume\n" if $DEBUG; + my ( $path, undef, undef, $wwid ) = $class->filesystem_path( $scfg, $volname ); + + print "Mapped volume '$volname' with WWN: " . uc( $wwid ) . ".\n" if $DEBUG; + + run_command( [ $cmd->{ "multipath" }, "-a", $wwid ] ); + run_command( [ $cmd->{ "multipathd" }, "add", "path", $path ] ); + + $class->purestorage_rescan_diskmap( $wwid ); + + if ( -e $path ) { + return 1; + } + + warn "Warning :: Local path '$path' not exists.\n"; + return 0; +} + +sub unmap_volume { + my ( $class, $storeid, $scfg, $volname, $snapname ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::unmap_volume\n" if $DEBUG; + + my ( $path, undef, undef, $wwid ) = $class->filesystem_path( $scfg, $volname ); + my $device_path; + my @slaves = []; + my $slaves_path; + my $slave_name; + my $slave_path; + my $multipath_check = 0; + my $wwid_file = "/etc/multipath/wwids"; + + if ( $path && -e $path ) { + eval { run_command( [ $cmd->{ "multipathd" }, "disablequeueing", "map", $wwid ] ) }; + eval { run_command( [ $cmd->{ "blockdev" }, "--flushbufs", $path ] ) }; + + if ( $device_path = readlink( $path ) ) { + print "Device path resolved to '$device_path'.\n"; + } else { + die "Error :: unable to read device link."; + } + + my $device_name = basename( $device_path ); + print "Device name resolved to '$device_name'.\n"; + + my $multipath_check = `$cmd->{ "multipath" } -l $path`; + my $slaves_path = "/sys/block/$device_name/slaves"; + + if ( -d $slaves_path ) { + opendir( my $dh, $slaves_path ) or die "Cannot open directory: $!"; + @slaves = grep { !/^\.\.?$/ } readdir( $dh ); + closedir( $dh ); + + print "Disk '$device_name' slaves: \n" . Dumper( @slaves ) if $DEBUG; + } elsif ( $device_name =~ m|^(sd[a-z]+)$| ) { + warn "Warning :: Disk '$device_name' has no slaves"; + push @slaves, $1; + } + + if ( $multipath_check ) { + print "Device '$path' is a multipath device. Proceeding with multipath removal.\n"; + + if ( -e $wwid_file ) { + open( my $in, '<', $wwid_file ) or die $!; + open( my $out, '>', $wwid_file ) or die $!; + print $out grep { !/^#3/ } <$in>; + close $in; + close $out; + } + + # If the device is a multipath device, remove the link + eval { run_command( [ $cmd->{ "multipath" }, "-f", $path ] ) == 0 or die "Failed to remove multipath link for '$path'.\n"; }; + + if ( $@ ) { + warn "Warning :: $@"; + } + + } else { + print "Device '$path' is not a multipath device. Skipping multipath removal.\n"; + } + + # Iterate through slaves and delete each device + foreach $slave_name ( @slaves ) { + print "Remove slave: $slave_name\n" if $DEBUG; + if ( $slave_name =~ m|^(sd[a-z]+)$| ) { + $slave_name = $1; # untaint; + $class->purestorage_unmap_disk( $slave_name ); + } else { + die "Error :: Invalid disk name '$slave_name'."; + } + } + + print "Device '$device_name' successfully removed from system.\n"; + return 1; + } + return 0; +} + +sub activate_volume { + my ( $class, $storeid, $scfg, $volname, $snapname, $cache ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::activate_volume\n" if $DEBUG; + + $class->purestorage_volume_connection( $scfg, $volname ); + + $class->map_volume( $storeid, $scfg, $volname, $snapname ); + return 1; +} + +sub deactivate_volume { + my ( $class, $storeid, $scfg, $volname, $snapname, $cache ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::deactivate_volume\n" if $DEBUG; + + $class->purestorage_volume_connection( $scfg, $volname, 'DELETE' ); + $class->unmap_volume( $storeid, $scfg, $volname, $snapname ); + return 1; +} + +sub volume_resize { + my ( $class, $scfg, $storeid, $volname, $size, $running ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::volume_resize\n" if $DEBUG; + warn "New Size: $size\n" if $DEBUG; + + $class->purestorage_resize_volume( $scfg, $volname, $size ) or die "Error :: Failed to resize volume '$volname'"; + + return undef; +} + +sub rename_volume { + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::rename_volume\n"; + my ( $class, $scfg, $storeid, $source_volname, $target_vmid, $target_volname ) = @_; + die "not implemented in storage plugin '$class'\n" if $class->can( 'api' ) && $class->api() < 10; + + my ( undef, $source_image, $source_vmid, $base_name, $base_vmid, undef, $format ) = $class->parse_volname( $source_volname ); + + $target_volname = $class->find_free_diskname( $storeid, $scfg, $target_vmid, $format, 1 ) + if !$target_volname; + + $class->purestorage_rename_volume( $scfg, $source_volname, $target_volname ); + + $base_name = $base_name ? "${base_name}/" : ''; + + return "${storeid}:${base_name}${target_volname}"; +} + +sub volume_import { + my ( $class, $scfg, $storeid, $fh, $volname, $format, $snapshot, $base_snapshot, $with_snapshots, $allow_rename ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::volume_import\n" if $DEBUG; + die "=> PVE::Storage::Custom::PureStoragePlugin::sub::volume_import not implemented!"; + + return 1; +} + +sub volume_snapshot { + my ( $class, $scfg, $storeid, $volname, $snap ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::volume_snapshot\n" if $DEBUG; + + if ( !$class->purestorage_snap_volume_create( $scfg, $snap, $volname ) ) { + die "Error :: Failed to snapshot volume '$volname'"; + } + return 1; +} + +sub volume_snapshot_rollback { + my ( $class, $scfg, $storeid, $volname, $snap ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::volume_snapshot_rollback\n" if $DEBUG; + + if ( !$class->purestorage_snap_volume_rollback( $scfg, $snap, $volname ) ) { + die "Error :: Failed to rollback snapshot volume '$volname'"; + } + die; + return 1; +} + +sub volume_snapshot_delete { + my ( $class, $scfg, $storeid, $volname, $snap ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::volume_snapshot_delete\n" if $DEBUG; + + if ( !$class->purestorage_snap_volume_delete( $scfg, $snap, $volname ) ) { + die "Error :: Failed to snapshot volume '$volname'"; + } + return 1; +} + +sub volume_has_feature { + my ( $class, $scfg, $feature, $storeid, $volname, $snapname, $running ) = @_; + print "Debug :: PVE::Storage::Custom::PureStoragePlugin::sub::volume_has_feature\n" if $DEBUG; + + my $features = { + copy => { base => 1, current => 1, snap => 1 }, # full clone is possible + clone => { base => 1, snap => 1 }, # linked clone is possible + snapshot => { current => 1 }, # taking a snapshot is possible + # template => { current => 1 }, # conversion to base image is possible + # sparseinit => { base => 1, current => 1 }, # volume is sparsely initialized (thin provisioning) + rename => { current => 1 }, # renaming volumes is possible + }; + my ( $vtype, $name, $vmid, $basename, $basevmid, $isBase ) = $class->parse_volname( $volname ); + my $key = undef; + if ( $snapname ) { + $key = "snap"; + } else { + $key = $isBase ? "base" : "current"; + } + return 1 if $features->{ $feature }->{ $key }; + return undef; +} +1; diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec95cb9 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# 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. + +## Table of Contents + +- [Prerequisites](#prerequisites) + - [Multipath Configuration](#multipath-configuration) +- [Installation](#installation) + - [Manual](#manual) + - [APT](#apt) +- [Configuration](#configuration) +- [Troubleshooting](#troubleshooting) +- [Contributing](#contributing) + +## Prerequisites + +Before installing and using this plugin, ensure that your Proxmox VE environment meets the following prerequisites. + +### 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. + +Below is an example of how your multipath.conf file should look when configured for Pure Storage arrays: + +``` +defaults { + polling_interval 2 + find_multipaths no +} + +devices { + device { + vendor "PURE" + product "FlashArray" + path_selector "queue-length 0" + hardware_handler "1 alua" + path_grouping_policy group_by_prio + prio alua + failback immediate + path_checker tur + fast_io_fail_tmo 10 + user_friendly_names no + no_path_retry 0 + features "0" + dev_loss_tmo 60 + recheck_wwid yes + } +} + +blacklist { + device { + vendor ".*" + product ".*" + } +} + +blacklist_exceptions { + device { + vendor "PURE" + } +} +``` + +## Installation + +There are two methods to install the plugin: manual installation and APT package installation. + +### Manual + +To manually install the plugin, follow these steps: + +```bash +# Clone repository +git clone git@github.com:kolesa-team/pve-purestorage.git +# Navigate to the Plugin Directory +cd pve-purestorage +# Copy plugin to custom plugin directory +sudo cp PureStoragePlugin.pm /usr/share/perl5/PVE/Storage/Custom +# Restart Proxmox VE +sudo systemctl restart pve-cluster.service pvedaemon.service pvestatd.service pveproxy.service pvescheduler.service +``` + +### APT + +**Note**: Replace `` with your desired version number (e.g., `0.0.1`). + +```bash +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 install ./libpve-storage-purestorage-perl.deb +``` + +## 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.conf`. + +``` +purestorage: pure + nodes: + address https:// + token + vgname + hgsuffix + content images +``` + +| Parameter | Description | +| --------- | ----------- | +| 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. | +| token | The API token used for authentication with the Pure Storage array. This token must have sufficient permissions to create and manage volumes. | +| vgname | The name of the volume group where new virtual disks will be created. This should match the configuration on your Pure Storage array. | +| 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. | + +> **_NOTE:_** Ensure that the token and other sensitive information are kept secure and not exposed publicly. + +Example Configuration: + +``` +purestorage: pure + address https://purestorage.example.com + token abc123 + vgname pure_vg + content images +``` + +## Troubleshooting + +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: + +```bash +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. +- 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. +- 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. + +## Contributing + +Contributions to this project are welcome. diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..b1bd38b --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +13 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..15bf494 --- /dev/null +++ b/debian/control @@ -0,0 +1,19 @@ +Source: libpve-storage-purestorage-perl +Section: perl +Priority: optional +Maintainer: Timur Kumakbayev +Standards-Version: 4.6.0 +Homepage: https://github.com/kolesa-team/pve-purestorage-plugin/blob/main/README.md +Vcs-Browser: https://github.com/kolesa-team/pve-purestorage-plugin +Vcs-Git: https://github.com/kolesa-team/pve-purestorage-plugin.git +Build-Depends: debhelper, devscripts + +Package: libpve-storage-purestorage-perl +Architecture: all +Depends: ${perl:Depends}, ${misc:Depends}, libpve-storage-perl (>= 8.2.1), multipath-tools +Description: Pure Storage integration library for Proxmox VE + This package provides a Perl library to integrate Pure FlashArray Storage systems + with the Proxmox VE storage management infrastructure. + . + It allows administrators to configure and manage Pure FlashArray Storage devices + directly within the Proxmox VE environment. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..7ea7b8e --- /dev/null +++ b/debian/copyright @@ -0,0 +1,45 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: libpve-storage-purestorage-perl +Upstream-Contact: Timur Kumakbayev +Source: https://github.com/wimpysworld/deb-get + +Files: * +Copyright: 2024 Kolesa, JSC +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +# If you want to use GPL v2 or later for the /debian/* files use +# the following clauses, or change it to suit. Delete these two lines +Files: debian/* +Copyright: 2024 Kolesa, JSC +License: GPL-2+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..ca5368c --- /dev/null +++ b/debian/docs @@ -0,0 +1 @@ +docs/README.md diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..b0fe067 --- /dev/null +++ b/debian/install @@ -0,0 +1 @@ +PureStoragePlugin.pm usr/share/perl5/PVE/Storage/Custom diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..667651a --- /dev/null +++ b/debian/postinst @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +if [ "$1" = "configure" ]; then + systemctl try-restart pve-cluster.service || true + systemctl try-restart pvedaemon.service || true + systemctl try-restart pvestatd.service || true + systemctl try-restart pveproxy.service || true + systemctl try-restart pvescheduler.service || true +fi diff --git a/debian/postrm b/debian/postrm new file mode 100644 index 0000000..667651a --- /dev/null +++ b/debian/postrm @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +if [ "$1" = "configure" ]; then + systemctl try-restart pve-cluster.service || true + systemctl try-restart pvedaemon.service || true + systemctl try-restart pvestatd.service || true + systemctl try-restart pveproxy.service || true + systemctl try-restart pvescheduler.service || true +fi diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..f18b74d --- /dev/null +++ b/debian/rules @@ -0,0 +1,14 @@ +#!/usr/bin/make -f + +%: + dh $@ --parallel + +override_dh_auto_install: + dh_auto_install + + install -d ${CURDIR}/debian/tmp/docs + install -m 0644 ${CURDIR}/README.md ${CURDIR}/debian/tmp/docs + +override_dh_auto_clean: + dh_auto_clean + rm --force --recursive ${CURDIR}/debian/tmp diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/debian/source/options b/debian/source/options new file mode 100644 index 0000000..85e3972 --- /dev/null +++ b/debian/source/options @@ -0,0 +1,3 @@ +compression = gzip +compression-level = 9 +tar-ignore = .git diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..6ba934f --- /dev/null +++ b/debian/watch @@ -0,0 +1,5 @@ +version=4 + +opts="filenamemangle=s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%deb-get-$1.tar.gz%" \ + https://github.com/kolesa-team/pve-purestorage-plugin/tags \ + (?:.*?/)?v?(\d[\d.]*)\.tar\.gz