#!/usr/bin/env perl # Copyright (c) 2025, R. W. Rodolico # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # linux_lvm # Linux LVM2: storage.lvm keys (PVs resolved to disks, md or partitions), and the # old diskinfo LVM keys (replaces all_lvm2). Runs only when lvs is installed. # # Revision History: # 2026-10-18 Claude v1.0.0 new sensor, split from unix_storage (bug 210) package linux_lvm; use strict; use warnings; use version; our $VERSION = version->declare('v1.0.0'); use FindBin; use File::Spec; use lib File::Spec->catdir($FindBin::Bin), File::Spec->catdir("$FindBin::Bin/.."); use library qw(:all); my $CATEGORY = 'storage'; my $OLD_CATEGORY = 'diskinfo'; ####################################################### # # parseLvm( $pvsText, $vgsText, $lvsText ) # # Parses pvs, vgs and lvs output (--units b --nosuffix --noheadings # --separator '|', default columns). # # Parameters: # $pvsText - pvs output (pv|vg|fmt|attr|psize|pfree) # $vgsText - vgs output (vg|#pv|#lv|#sn|attr|vsize|vfree) # $lvsText - lvs output (lv|vg|attr|lsize|...) # # Returns: # hash reference with array references pvs, vgs, lvs of hashes # # Side Effects: # none # ####################################################### sub parseLvm { my ($pvsText, $vgsText, $lvsText) = @_; my %lvm = ('pvs' => [], 'vgs' => [], 'lvs' => []); my $rows = sub { my $text = shift; return map { [map { trim($_) } split(/\|/, $_, -1)] } grep { /\S/ } split(/\n/, defined($text) ? $text : ''); }; foreach my $f ($rows->($pvsText)) { push @{$lvm{'pvs'}}, {'pv' => $f->[0], 'vg' => $f->[1], 'size' => $f->[4], 'free' => $f->[5]} if defined($f->[5]); } foreach my $f ($rows->($vgsText)) { push @{$lvm{'vgs'}}, {'vg' => $f->[0], 'size' => $f->[5], 'free' => $f->[6]} if defined($f->[6]); } foreach my $f ($rows->($lvsText)) { push @{$lvm{'lvs'}}, {'lv' => $f->[0], 'vg' => $f->[1], 'attr' => $f->[2], 'size' => $f->[3]} if defined($f->[3]); } return \%lvm; } ####################################################### # # oldLvmLines( $lvm ) # # Old diskinfo LVM keys, as v3.7.1 all_lvm2: sizes in whole KiB (v3.7.1 # used --units k and int), LVs keyed by name alone, VG used = size - # free, VG components = its PVs. # # Parameters: # $lvm - hash reference from parseLvm # # Returns: # array reference of lines # # Side Effects: # none # ####################################################### sub oldLvmLines { my $lvm = shift; my @out; my $k = sub { return int($_[0] / 1024); }; foreach my $lv (@{$lvm->{'lvs'}}) { push @out, "$OLD_CATEGORY\t$lv->{'lv'}\tsize\t" . $k->($lv->{'size'}); push @out, "$OLD_CATEGORY\t$lv->{'lv'}\tfstype\tlvm2 lv"; push @out, "$OLD_CATEGORY\t$lv->{'lv'}\tvg\t$lv->{'vg'}"; } foreach my $vg (@{$lvm->{'vgs'}}) { push @out, "$OLD_CATEGORY\t$vg->{'vg'}\tsize\t" . $k->($vg->{'size'}); push @out, "$OLD_CATEGORY\t$vg->{'vg'}\tused\t" . ($k->($vg->{'size'}) - $k->($vg->{'free'})); push @out, "$OLD_CATEGORY\t$vg->{'vg'}\tfstype\tlvm2 vg"; push @out, "$OLD_CATEGORY\t$vg->{'vg'}\tcomponents\t" . join(' ', map { $_->{'pv'} } grep { $_->{'vg'} eq $vg->{'vg'} } @{$lvm->{'pvs'}}); } foreach my $pv (@{$lvm->{'pvs'}}) { push @out, "$OLD_CATEGORY\t$pv->{'pv'}\tsize\t" . $k->($pv->{'size'}); push @out, "$OLD_CATEGORY\t$pv->{'pv'}\tfstype\tlvm2 pv"; } return \@out; } ####################################################### # # lvmLines( $lvm, $res, $realPaths ) # # storage.lvm keys; each PV is resolved to its md, disk or partition. # # Parameters: # $lvm - hash reference from parseLvm # $res - resolver # $realPaths - optional hash reference path => readlink -f result # # Returns: # array reference of lines # # Side Effects: # none # ####################################################### sub lvmLines { my ($lvm, $res, $realPaths) = @_; my @out; foreach my $pv (@{$lvm->{'pvs'}}) { my $prefix = "lvm\tpvs\t$pv->{'pv'}"; push @out, "$CATEGORY\t$prefix\tvg\t$pv->{'vg'}" if $pv->{'vg'} ne ''; push @out, "$CATEGORY\t$prefix\tsize_bytes\t$pv->{'size'}", "$CATEGORY\t$prefix\tfree_bytes\t$pv->{'free'}"; push @out, memberLines($prefix, resolveMember($pv->{'pv'}, $res, $realPaths)); } foreach my $vg (@{$lvm->{'vgs'}}) { push @out, "$CATEGORY\tlvm\tvgs\t$vg->{'vg'}\tsize_bytes\t$vg->{'size'}", "$CATEGORY\tlvm\tvgs\t$vg->{'vg'}\tfree_bytes\t$vg->{'free'}"; } foreach my $lv (@{$lvm->{'lvs'}}) { my $prefix = "$CATEGORY\tlvm\tlvs\t$lv->{'vg'}/$lv->{'lv'}"; push @out, "$prefix\tvg\t$lv->{'vg'}", "$prefix\tsize_bytes\t$lv->{'size'}", "$prefix\tattr\t$lv->{'attr'}"; } return \@out; } ####################################################### # # main() # # Emits storage.lvm and old diskinfo LVM keys. # # Parameters: # none # # Returns: # integer exit status (1 wrong OS, 2 not applicable) # # Side Effects: # runs pvs, vgs, lvs, readlink and lsblk, writes STDOUT/STDERR # ####################################################### sub main { if (!checkOS({ 'linux' => undef })) { print STDERR "unsupported os\n"; return 1; } unless (haveCommand('lvs')) { print STDERR "lvs not found\n"; return 2; } my $inv = localDiskInventory(); if (defined($inv->{'missing'})) { print STDERR "$inv->{'missing'} not found\n"; return 2; } my @args = ('--units', 'b', '--nosuffix', '--noheadings', '--separator', '|'); my $lvm = parseLvm(runCommand('pvs', @args), runCommand('vgs', @args), runCommand('lvs', @args)); unless (@{$lvm->{'pvs'}} || @{$lvm->{'vgs'}} || @{$lvm->{'lvs'}}) { print STDERR "no LVM volumes\n"; return 2; } my %realPaths; foreach my $pv (@{$lvm->{'pvs'}}) { next unless $pv->{'pv'} =~ m{^/dev/disk/}; my $real = runCommand('readlink', '-f', $pv->{'pv'}); chomp $real; $realPaths{$pv->{'pv'}} = $real if $real ne ''; } my @out; push @out, @{oldLvmLines($lvm)}; push @out, @{lvmLines($lvm, $inv->{'resolver'}, \%realPaths)}; print join("\n", @out), "\n"; return 0; } exit main() unless caller; 1;