[pve-devel] [PATCH WIP storage 1/2] add Diskmanage Utilities

Wolfgang Bumiller w.bumiller at proxmox.com
Mon May 23 14:03:08 CEST 2016


On Mon, May 23, 2016 at 11:03:12AM +0200, Dominik Csapak wrote:
> this adds the functions for listing the disks (mostly copied from
> the ceph code), checking if a disk is a valid blockdevice, if it
> is used/in a zfs pool, and an init function (just to add a gpt header;
> this is important if one wants to use a fresh disk for ceph journals)
> 
> Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
> ---
>  PVE/Diskmanage.pm | 312 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
>  PVE/Makefile      |   1 +
>  2 files changed, 313 insertions(+)
>  create mode 100644 PVE/Diskmanage.pm
> 
> diff --git a/PVE/Diskmanage.pm b/PVE/Diskmanage.pm
> new file mode 100644
> index 0000000..97759a8
> --- /dev/null
> +++ b/PVE/Diskmanage.pm
> @@ -0,0 +1,312 @@
> +package PVE::Diskmanage;
> +
> +use strict;
> +use warnings;
> +use PVE::ProcFSTools;
> +use Data::Dumper;
> +use Cwd qw(abs_path);
> +
> +use PVE::Tools qw(extract_param run_command file_get_contents file_read_firstline dir_glob_regex dir_glob_foreach trim);
> +
> +my $SMARTCTL = "/usr/sbin/smartctl";
> +my $ZPOOL = "/sbin/zpool";
> +my $SGDISK = "/sbin/sgdisk";
> +
> +sub verify_blockdev_path {
> +    my ($rel_path) = @_;
> +
> +    die "missing path" if !$rel_path;
> +    my $path = abs_path($rel_path);
> +    die "failed to get absolute path to $rel_path\n" if !$path;
> +
> +    die "got unusual device path '$path'\n" if $path !~  m|^/dev/(.*)$|;
> +
> +    $path = "/dev/$1"; # untaint
> +
> +    die "no such block device '$path'\n"
> +	if ! -b $path;
> +
> +    return $path;
> +};
> +
> +sub init_disk {
> +    my ($disk, $uuid) = @_;
> +
> +    die "not a valid disk" if $disk !~ m|^/dev/| ||  ! -b $disk;
> +    die "disk $disk is already in use\n" if disk_is_used($disk);
> +
> +    my $id = $uuid || 'R';
> +    eval {
> +	run_command([$SGDISK, $disk, '-U', $id]);
> +    };
> +    die "ERROR: $@" if $@;
> +}
> +
> +sub disk_is_used {
> +    my ($disk) = @_;
> +
> +    my $dev = $disk =~ s|^/dev/||;
> +
> +    my $disklist = get_disks($dev);
> +
> +    return 1 if $disklist->{$dev}->{used};
> +
> +    return 0;
> +}
> +
> +sub get_smart_data {
> +    my ($disk) = @_;
> +
> +    die "not a device" if $disk !~ m|^/dev/|;
> +    my $smartdata = {};
> +    my $datastarted = 0;
> +
> +    eval {
> +    run_command([$SMARTCTL, '-a', '-f', 'brief', $disk], outfunc => sub{
> +	my ($line) = @_;
> +
> +	if ($datastarted && $line =~ m/^[ \d]{2}\d/) {
> +	    $line = trim($line);
> +	    my @data = split /\s+/, $line;
> +	    my $entry = {};
> +	    $entry->{name} = $data[1];
> +	    $entry->{flags} = $data[2];
> +	    $entry->{value} = $data[3];
> +	    $entry->{worst} = $data[4];
> +	    $entry->{threshold} = $data[5];
> +	    $entry->{fail} = $data[6];
> +	    $entry->{raw} = $data[7];
> +	    $smartdata->{attributes}->{$data[0]} = $entry;
> +	} elsif ($line =~ m/self\-assessment test result: (.*)$/) {
> +	    $smartdata->{health} = $1;
> +	} elsif ($line =~ m/Vendor Specific SMART Attributes with Thresholds:/) {
> +	    $datastarted = 1;
> +	}
> +    });
> +    };
> +    die "Error getting S.M.A.R.T. data\n" if $@;
> +    $smartdata->{health} = 'UNKOWN' if !defined $smartdata->{health};
> +    return $smartdata;
> +}
> +
> +sub get_smart_health {
> +    my ($disk) = @_;
> +
> +    return "NOT A DEVICE" if $disk !~ m|^/dev/| || ! -b $disk;
> +
> +    my $message = "UNKOWN";
> +
> +    eval {
> +	run_command([$SMARTCTL, '-H', $disk], outfunc => sub{
> +	    my ($line) = @_;
> +
> +	    if ($line =~ m/test result: (.*)$/) {
> +		$message = $1;
> +	    } elsif ($line =~ m/open device: (.*) failed: (.*)$/) {
> +		$message = "FAILED TO OPEN";
> +	    } elsif ($line =~ m/^SMART Disabled/) {
> +		$message = "SMART DISABLED";
> +	    }
> +	});
> +    };
> +
> +    return $message;
> +}
> +
> +sub get_zfs_devices {
> +    my $list = {};
> +    eval {
> +	run_command([$ZPOOL, 'list', '-HLv'], outfunc => sub {
> +	     my ($line) = @_;
> +
> +	     if ($line =~ m|^\t([^\t]+)\t|) {
> +		$list->{$1} = 1;
> +	     }
> +	});
> +    };
> +    return $list;
> +}
> +
> +sub get_disks {
> +    my ($disk) = @_;
> +    my $disklist = {};
> +
> +    my $fd = IO::File->new("/proc/mounts", "r") ||
> +	die "unable to open /proc/mounts - $!\n";

Please use the related functionality from pve-common's ProcFSTools.
There's parse_mounts() which handles the encoding of special characters
in paths.
(Maybe factor out the grep() of ProcFSTools::is_mounted() since that
one currently does both the reading/parsing + checking, or just take
over the grep line here into your $dev_is_mounted below.)

> +
> +    my $mounted = {};
> +
> +    while (defined(my $line = <$fd>)) {
> +	my ($dev, $path, $fstype) = split(/\s+/, $line);
> +	next if !($dev && $path && $fstype);
> +	next if $dev !~ m|^/dev/|;
> +	my $real_dev = abs_path($dev);
> +	$mounted->{$real_dev} = $path;
> +    }
> +    close($fd);
> +
> +    my $dev_is_mounted = sub {
> +	my ($dev) = @_;
> +	return $mounted->{$dev};
> +    };
> +
> +    my $dir_is_empty = sub {
> +	my ($dir) = @_;
> +
> +	my $dh = IO::Dir->new ($dir);
> +	return 1 if !$dh;
> +
> +	while (defined(my $tmp = $dh->read)) {
> +	    next if $tmp eq '.' || $tmp eq '..';
> +	    $dh->close;
> +	    return 0;
> +	}
> +	$dh->close;
> +	return 1;
> +    };
> +
> +    my $journal_uuid = '45b0969e-9b03-4f30-b4c6-b4b80ceff106';
> +
> +    my $journalhash = {};
> +    dir_glob_foreach('/dev/disk/by-parttypeuuid', "$journal_uuid\..+", sub {
> +	my ($entry) = @_;
> +	my $real_dev = abs_path("/dev/disk/by-parttypeuuid/$entry");
> +	$journalhash->{$real_dev} = 1;
> +    });
> +
> +    dir_glob_foreach('/sys/block', '.*', sub {
> +	my ($dev) = @_;
> +	return if defined($disk) && $disk ne $dev;
> +	return if $dev eq '.';
> +	return if $dev eq '..';
> +
> +	return if $dev =~ m|^ram\d+$|; # skip ram devices
> +	return if $dev =~ m|^loop\d+$|; # skip loop devices
> +	return if $dev =~ m|^md\d+$|; # skip md devices
> +	return if $dev =~ m|^dm-.*$|; # skip dm related things
> +	return if $dev =~ m|^fd\d+$|; # skip Floppy
> +	return if $dev =~ m|^sr\d+$|; # skip CDs

Not sure if whitelisting would be better, and checking the symlink
value. Eg. this also contains 'zd*' for zvols which includes zvols used
for VMs and 'nbd' devices.
Perhaps all devices symlinked to an entry /sys/devices/virtual should be
ignored unless they're part of something whitelisted.
Excluding all dm-* devices also means excluding multipath devices, which
might still be unused otherwise and expected to show up here instead of
their underlying block device nodes (which show up as 'LVM' since they
contain dm-* entries in holders/).

> +	my $devdir = "/sys/block/$dev/device";
> +	return if ! -d $devdir;
> +
> +	my $size = file_read_firstline("/sys/block/$dev/size");
> +	return if !$size;
> +
> +	$size = $size * 512;
> +
> +	my $info = `udevadm info --path /sys/block/$dev --query all`;
> +	return if !$info;
> +
> +	return if $info !~ m/^E: DEVTYPE=disk$/m;
> +	return if $info =~ m/^E: ID_CDROM/m;
> +
> +	my $serial = 'unknown';
> +	if ($info =~ m/^E: ID_SERIAL_SHORT=(\S+)$/m) {
> +	    $serial = $1;
> +	}
> +
> +	my $gpt = 0;
> +	if ($info =~ m/^E: ID_PART_TABLE_TYPE=gpt$/m) {
> +	    $gpt = 1;
> +	}
> +
> +	# detect SSD
> +	my $rpm = -1;
> +	if ($info =~ m/^E: ID_ATA_ROTATION_RATE_RPM=(\d+)$/m) {
> +	    $rpm = $1;
> +	}
> +
> +	# dir/queue/rotational is 1 for hdd, 0 for ssd
> +	my $type = 'unknown';
> +	my $rotational = file_read_firstline("/sys/block/$dev/queue/rotational");
> +
> +	if ($rotational == 0) {
> +	    $type = 'ssd';
> +	    $rpm = 0;
> +	} elsif ($rotational == 1) {
> +	    if ($rpm != -1) {
> +		$type = 'hdd';
> +	    } elsif ($info =~ m/^E: ID_BUS=usb/) {
> +		$type = 'usb';
> +		$rpm = 0;
> +	    }
> +	}
> +
> +	my $wwn = 'unkown';
> +	if ($info =~ m/^E: ID_WWN=(.*)$/m) {
> +	    $wwn = $1;
> +	}
> +
> +	my $vendor = file_read_firstline("$devdir/vendor") || 'unknown';
> +	my $model = file_read_firstline("$devdir/model") || 'unknown';
> +
> +	my $health = get_smart_health("/dev/$dev");
> +
> +	my $zfslist = get_zfs_devices();
> +
> +	my $used;
> +
> +	$used = 'LVM' if !&$dir_is_empty("/sys/block/$dev/holders");

Multipath devices also have device mapper nodes in this path, without
having an LVM on them. (See above)
(All device-mapper subsystems should show up there AFAIK)

> +	$used = 'mounted' if &$dev_is_mounted("/dev/$dev");
> +
> +	$used = 'ZFS' if $zfslist->{$dev};
> +
> +	$disklist->{$dev} = {
> +	    vendor => $vendor,
> +	    model => $model,
> +	    size => $size,
> +	    serial => $serial,
> +	    gpt => $gpt,
> +	    rpm => $rpm,
> +	    type =>  $type,
> +	    wwn => $wwn,
> +	    health => $health,
> +	};
> +
> +	my $osdid = -1;
> +
> +	my $journal_count = 0;
> +
> +	my $found_partitions;
> +	my $found_lvm;
> +	my $found_mountpoints;
> +	my $found_zfs;
> +	dir_glob_foreach("/sys/block/$dev", "$dev.+", sub {
> +	    my ($part) = @_;
> +
> +	    $found_partitions = 1;
> +
> +	    if (my $mp = &$dev_is_mounted("/dev/$part")) {
> +		$found_mountpoints = 1;
> +		if ($mp =~ m|^/var/lib/ceph/osd/ceph-(\d+)$|) {
> +		    $osdid = $1;
> +		}
> +	    }
> +	    if (!&$dir_is_empty("/sys/block/$dev/$part/holders"))  {
> +		$found_lvm = 1;

^ Multipath again

> +	    }
> +
> +	    if ($zfslist->{$part}) {
> +		$found_zfs = 1;
> +	    }
> +
> +	    $journal_count++ if $journalhash->{"/dev/$part"};
> +	});
> +
> +	$used = 'mounted' if $found_mountpoints && !$used;
> +	$used = 'LVM' if $found_lvm && !$used;
> +	$used = 'ZFS' if $found_zfs && !$used;
> +	$used = 'partitions' if $found_partitions && !$used;
> +
> +	$disklist->{$dev}->{used} = $used if $used;
> +	$disklist->{$dev}->{osdid} = $osdid;
> +	$disklist->{$dev}->{journals} = $journal_count;
> +    });
> +
> +    return $disklist;
> +
> +}
> +
> +1;
> diff --git a/PVE/Makefile b/PVE/Makefile
> index 655cad8..ae2bd35 100644
> --- a/PVE/Makefile
> +++ b/PVE/Makefile
> @@ -3,6 +3,7 @@
>  .PHONY: install
>  install:
>  	install -D -m 0644 Storage.pm ${DESTDIR}${PERLDIR}/PVE/Storage.pm
> +	install -D -m 0644 Diskmanage.pm ${DESTDIR}${PERLDIR}/PVE/Diskmanage.pm
>  	make -C Storage install
>  	make -C API2 install
>  	make -C CLI install
> -- 
> 2.1.4
> 
> 
> _______________________________________________
> pve-devel mailing list
> pve-devel at pve.proxmox.com
> http://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
> 




More information about the pve-devel mailing list