diff --git a/Proxmox/Makefile b/Proxmox/Makefile index 9561d9b..035626b 100644 --- a/Proxmox/Makefile +++ b/Proxmox/Makefile @@ -17,6 +17,7 @@ PERL_MODULES=\ Sys/File.pm \ Sys/Net.pm \ Sys/Udev.pm \ + Sys/ZFS.pm \ UI.pm \ UI/Base.pm \ UI/Gtk3.pm \ diff --git a/Proxmox/Sys/ZFS.pm b/Proxmox/Sys/ZFS.pm new file mode 100644 index 0000000..9232d1a --- /dev/null +++ b/Proxmox/Sys/ZFS.pm @@ -0,0 +1,93 @@ +package Proxmox::Sys::ZFS; + +use strict; +use warnings; + +use Proxmox::Sys::Command qw(run_command); + +use base qw(Exporter); +our @EXPORT_OK = qw(get_exported_pools); + +# Parses the output of running `zpool import`, which shows all importable +# ZFS pools. +# Unfortunately, `zpool` does not support JSON or any other machine-readable +# format for output, so we have to do it the hard way. +# +# Example output of `zpool import` looks like this: +# +# root@proxmox:/# zpool import +# pool: testpool +# id: 4958685680270539150 +# state: ONLINE +# action: The pool can be imported using its name or numeric identifier. +# config: +# +# testpool ONLINE +# vdc ONLINE +# vdd ONLINE +# +# pool: rpool +# id: 9412322616744093413 +# state: ONLINE +# status: The pool was last accessed by another system. +# action: The pool can be imported using its name or numeric identifier and +# the '-f' flag. +# see: https://openzfs.github.io/openzfs-docs/msg/ZFS-8000-EY +# config: +# +# rpool ONLINE +# mirror-0 ONLINE +# vda3 ONLINE +# vdb3 ONLINE +# +sub zpool_import_parse_output { + my ($fh) = @_; + + my $pools = []; # all found pools + my $pool = {}; # last found pool in output + + while (my $line = <$fh>) { + # first, if we find the start of a new pool, add it to the list with + # its name + if ($line =~ /^\s+pool: (.+)$/) { + # push the previous parsed pool to the result list + push @$pools, $pool if %$pool; + $pool = { name => $1 }; + next; + } + + # ignore any (garbage) output before the actual list, just in case + next if !%$pool; + + # add any possibly-useful attribute to the last (aka. current) pool + if ($line =~ /^\s*(id|state|status|action): (.+)$/) { + chomp($pool->{$1} = $2); + next; + } + } + + # add the final parsed pool to the list + push @$pools, $pool if %$pool; + return $pools; +} + +# Returns an array of all importable ZFS pools on the system. +# Each entry is a hash of the format: +# +# { +# name => '', +# id => , +# /* see also zpool_state_to_name() in lib/libzfs/libzfs_pool.c /* +# state => 'OFFLINE' | 'REMOVED' | 'FAULTED' | 'SPLIT' | 'UNAVAIL' \ +# | 'FAULTED' | 'DEGRADED' | 'ONLINE', +# status => '', optional +# action => '', optional +# } +sub get_exported_pools { + my $raw = run_command(['zpool', 'import']); + open (my $fh, '<', \$raw) or die 'failed to open in-memory stream'; + + return zpool_import_parse_output($fh); +} + +1; diff --git a/test/Makefile b/test/Makefile index 99bf14e..c473af8 100644 --- a/test/Makefile +++ b/test/Makefile @@ -3,12 +3,13 @@ all: export PERLLIB=.. .PHONY: check -check: test-zfs-arc-max test-run-command test-parse-fqdn test-ui2-stdio +check: test-zfs-arc-max test-run-command test-parse-fqdn test-ui2-stdio test-zfs-get-pool-list .PHONY: test-zfs-arc-max test-zfs-arc-max: ./zfs-arc-max.pl +.PHONY: test-run-command test-run-command: ./run-command.pl @@ -19,3 +20,7 @@ test-parse-fqdn: .PHONY: test-ui2-stdio test-ui2-stdio: ./ui2-stdio.pl + +.PHONY: test-zfs-get-pool-list +test-zfs-get-pool-list: + ./zfs-get-pool-list.pl diff --git a/test/zfs-get-pool-list.pl b/test/zfs-get-pool-list.pl new file mode 100755 index 0000000..34e6b20 --- /dev/null +++ b/test/zfs-get-pool-list.pl @@ -0,0 +1,57 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use File::Temp; +use Test::More tests => 8; + +use Proxmox::Sys::ZFS; +use Proxmox::UI; + +my $log_file = File::Temp->new(); +Proxmox::Log::init($log_file->filename); + +Proxmox::UI::init_stdio(); + +our $ZPOOL_IMPORT_TEST_OUTPUT = < { id => '4958685680270539150', state => 'ONLINE' }, + rpool => { id => '9412322616744093413', state => 'FAULTED' }, +}; + +open(my $fh, '<', \$ZPOOL_IMPORT_TEST_OUTPUT); +my $result = Proxmox::Sys::ZFS::zpool_import_parse_output($fh); +while (my ($name, $info) = each %$pools) { + my ($p) = grep { $_->{name} eq $name } @$result; + ok(defined($p), "pool $name was found"); + is($p->{id}, $info->{id}, "pool $name has correct id"); + is($p->{state}, $info->{state}, "pool $name has correct state"); + like($p->{action}, qr/^The pool can be imported using its name or numeric identifier/, + "pool $name can be imported"); +}