#!/usr/bin/perl # # Author: Petter Reinholdtsen # Date: 2006-09-19 # License: GNU General Public License # # This is the fsautoresize system. It checks file systems, and # automatically extend the full ones based on the provided # instructions. use strict; use warnings; use Getopt::Std; use Sys::Syslog qw(openlog syslog closelog LOG_NOTICE); use File::Basename; # Using this module (instead of Filesys::DiskSpace) to get a version # providing the device size, and not only free and used. use Filesys::Df; # from debian package libfilesys-df-perl # Config # for each mount point in the list, extend it when it become too full # (based on real size in KiB/MiB/GiB or percent), and increase it with # either a static size or a percentage, until it reaches the upper # limit (in real size or fraction of volume group). Should also # support scripting to decide if the partition should be extended or # not, to handle more advanced logic like 100 MiB per user with home # directories on /home/. # read config # # Format: # [regex] minsizefree/min%free maxsize/max%size (of vg) size/% incrememt # The last matching regex take effect. Example: # .+ 1% 20g 10% # /usr 10% 10g 1g my @conffiles = qw(/usr/share/debian-edu-config/fsautoresizetab /site/etc/fsautoresizetab /etc/fsautoresizetab); my %opts; getopts("dnv", \%opts) || usage(); # handle signals (for reload and shutdown) # Check if all the listed mount points support online extending # loop # check full file systems (libfilesys-df-perl, libfilesys-diskfree-perl, # libfilesys-diskspace-perl,libfilesys-statvfs-perl) # resize if available space in volume group # send email if resize succeeded $ENV{PATH} = "/sbin:/usr/sbin:/bin:/usr/bin"; my %fsops = ( 'ext3' => { 'online_supported' => \&ext3_online_supported, 'online_resize' => \&ext3_online_resize, }, 'ext4' => { 'online_supported' => \&ext3_online_supported, 'online_resize' => \&ext3_online_resize, }, ); my %devopts = ( 'lvm' => { 'resize' => \&lvm_resize, } ); sub usage { print </dev/null |") || die "Unable to check $device"; while () { chomp; $supported = 1 if (m/Filesystem features: .*resize_inode /); } close(TUNE2FS); return $supported; } sub ext3_online_resize { my (%minfo) = @_; my $device = $minfo{device}; my $device_resize = $devopts{$minfo{devicetype}}->{resize}; my $retval; if (&$device_resize($minfo{device}, $minfo{newsize})) { $retval = run "resize2fs", "$device"; if (!$retval) { # Perhaps resize2fs is too old. Try ext2online instead. my $retval = run "ext2online", "$device"; } } else { print STDERR "error: unable to resize $device\n"; } # ext2online ? # fsck -f, lvresize, resize2fs return $retval; } sub lvm_resize { my ($device, $newsize) = @_; $device = map_dev_to_lvmdev($device); # Using lvextend and not lvresize, to make sure we do not try to # shrink the file system. return run("lvextend","-L${newsize}k", "$device"); } sub map_dev_to_lvmdev { my $device = shift; my ($vg, $lv) = $device =~ m%/dev/mapper/([^-]+)-(.+)$%; if ($vg) { # Remap if using stupid new linux kernel and/or tools $device = "/dev/$vg/$lv"; } return $device } sub guess_devicetype { my $device = shift; # Try to find the real device, to handle /dev/vg/lv -> /dev/mapper/vg-lv $device = readlink $device if ( -l $device ); return "lvm" if ($device =~ m%^/dev/mapper/.+-.+$% || $device =~ m%^../dm-.+$%); return undef; } sub get_volumecapasity { my $device = shift; return 1000; # FIXME Placeholder } sub get_lvextents { my $device = shift; $device = map_dev_to_lvmdev($device); open(my $fh, "lvdisplay -c $device 2>/dev/null |") or die "Unable to extract lvm lv extent size for $device"; my @f = split(/:/, <$fh>); close($fh); return @f[6,7]; } sub supported_mountpoints { my %mountpoints; my %devices; open(M, "/proc/mounts") || die "Unable to open /proc/mounts"; while () { chomp; my @f = split(/\s+/); my $device = $f[0]; # Always use mapper names instead of kernel ones. if (index ($f[0], "/dev/dm-") != -1) { for my $mapdevice (glob "/dev/mapper/*") { my $dmdevice = basename(readlink $mapdevice) if -l $mapdevice; $device = $mapdevice if defined($dmdevice) && $dmdevice =~ basename($f[0]); } } my $mountpoint = $f[1]; my $typename = $f[2]; next unless (exists $fsops{$typename}); my $devicetype = guess_devicetype($device), my $extents; # Only devices mounted several times once next if $devices{$device}; $devices{$device} = 1; print STDERR "Checking $mountpoint [$device]\n" if $opts{v}; if ( -d $mountpoint && -e $device) { # df only work if the directory is available my $ref = df($mountpoint); my ($size, $used, $avail) = ($ref->{blocks}, $ref->{used}, $ref->{bavail}); # my ($fs_type, $fs_desc, $used, $avail, $fused, $favail) = # df $mountpoint; if (defined $devicetype && "lvm" eq $devicetype) { my ($volsizeblocks, $extents) = get_lvextents($device); # Convert from 512 byte blocks to kilobytes $size = $volsizeblocks/2; } my $fracavail = 100 * $avail / $size; print STDERR " A: $size $used $avail ($fracavail%)\n" if $opts{v}; my %minfo = ( mountpoint => $mountpoint, device => $device, devicetype => $devicetype, fstype => $typename, extents => $extents, # These three are in kilobytes used => $used, available => $avail, size => $size, volsize => get_volumecapasity($device), ); next unless (defined $minfo{'devicetype'} && exists $devopts{$minfo{'devicetype'}}); $mountpoints{$mountpoint} = \%minfo; } } close(M); return %mountpoints; } sub calculate_resize { my ($minforef, $configref) = @_; my $mountpoint = $minforef->{mountpoint}; my $lastconfig; for my $config (@$configref) { my $regex = ${$config}{'regex'}; if ($mountpoint =~ m/^$regex$/) { # print STDERR "Matching '$regex'\n" if $opts{v}; $lastconfig = $config; } } if ($lastconfig) { my $minfree = $lastconfig->{minfree}; my $maxsize = $lastconfig->{maxsize}; my $increment = $lastconfig->{increment}; print(STDERR "info: $mountpoint matched regex ", $lastconfig->{regex}, " from ", $lastconfig->{sourcefile}, "\n") if $opts{v}; print STDERR " $minfree $maxsize\n" if $opts{v}; if ($minfree =~ m/(\d+)%$/) { $minfree = int($minforef->{size} * $1 / 100); } if ($maxsize =~ m/(\d+)%$/) { $maxsize = int($minforef->{volsize} * $1 / 100); } if ($increment =~ m/(\d+)%$/) { $increment = int($minforef->{size} * $1 / 100); } if (defined $lastconfig->{fstype} && "lvm" eq $lastconfig->{fstype}) { my $extents = $lastconfig->{extents}; my $extentsize= $lastconfig->{size} / $extents; if ($increment < $extentsize) { # Need to increase by at least one extent $increment = $extentsize; } } my $available = $minforef->{available}; print STDERR " $minfree>?$available $maxsize $increment\n" if $opts{v}; if ($minfree > $available) { my $size = $minforef->{size}; my $newsize = $size + $increment; $newsize = $maxsize if ($newsize > $maxsize); if ($newsize > $size) { $minforef->{newsize} = $newsize; print STDERR " Need more than $available available, resizing to $newsize\n" if $opts{v}; } else { # Upper limit is below the wanted size. Not resizing $minforef->{newsize} = $minforef->{size}; } } else { $minforef->{newsize} = $minforef->{size}; } } else { print STDERR "info: unable to match $mountpoint in config file\n"; $minforef->{newsize} = $minforef->{size}; } } sub as_kilobyte { my $val = shift; $val = $1 * 1024 * 1024 * 1024 if ($val =~ m/^(\d+)t$/i); $val = $1 * 1024 * 1024 if ($val =~ m/^(\d+)g$/i); $val = $1 * 1024 if ($val =~ m/^(\d+)m$/i); return $val; } sub load_config { my @conffiles = @_; my @config; for my $file (@conffiles) { open(F, "<", $file) || next; while () { chomp; s/\#.*//; my ($regex, $minfree, $maxsize, $increment) = split(/\s+/); next unless $regex; $increment = "10%" if $increment eq "defaults"; $minfree = as_kilobyte($minfree); $maxsize = as_kilobyte($maxsize); $increment = as_kilobyte($increment); my %fields = ( regex => $regex, minfree => $minfree, maxsize => $maxsize, increment => $increment, sourcefile=> $file, ); push(@config, \%fields); } close F; } return @config; } sub fs_resize { my @config = @_; my %mountpoints = supported_mountpoints(); for my $mountpoint (sort keys %mountpoints) { my %minfo = %{$mountpoints{$mountpoint}}; calculate_resize(\%minfo, \@config); my $online_supported = $fsops{$minfo{fstype}}->{'online_supported'}; # print(STDERR "S: $mountpoint ", $minfo{size}, " ", # $minfo{newsize}, "\n") if $opts{v}; if ($minfo{size} != $minfo{newsize}) { print STDERR "info: trying to resize $mountpoint\n" if $opts{v}; logmsg("overfull $mountpoint, resizing to $minfo{newsize}") if $opts{n}; if (&$online_supported(%minfo)) { $fsops{$minfo{fstype}}->{'online_resize'}(%minfo); } else { print STDERR "warning: unable to resize $mountpoint, online resizing support is not detected\n"; } } } } sub logmsg { my $msg = shift; openlog("debian-edu-fsautoresize", undef, 'user'); syslog(LOG_NOTICE, "%s", $msg); closelog; } if ($opts{d}) { # Deamon mode, run in the background while (1) { my @config = load_config(@conffiles); fs_resize(@config); sleep 300; } } else { my @config = load_config(@conffiles); fs_resize(@config); } exit 0;