#!/usr/bin/perl ######################################################################## # # mkdump.pl - Preparing disks for use as S/390 dump device # # Copyright (c) 2011 Tim Hardeck, SUSE LINUX Products GmbH # Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany. # Based on mkdump.sh (c) 2004 Hannes Reinecke, SuSE AG # # License: # # This program 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 program 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301 USA # ######################################################################## use strict; use warnings; use Fcntl; use Getopt::Long; my $VERSION = "2.0.4"; my $BLKID = "/sbin/blkid"; my $PARTED = "/usr/sbin/parted"; my $FDASD = "/sbin/fdasd"; my $DASDVIEW = "/sbin/dasdview"; my $DASDFMT = "/sbin/dasdfmt"; my $ZIPL = "/sbin/zipl"; my $UDEVADM = "/sbin/udevadm"; my $ZGETDUMP = "/sbin/zgetdump"; # temporary DASD device configuration file for Zipl my $MDPATH = "/tmp/mvdump.conf.".`mcookie`; chomp($MDPATH); my $OPT_DEBUG = 0; my $OPT_FORCE = 0; my $OPT_VERBOSE = 0; sub cleanup { # DASD if (-e $MDPATH) { system("rm -f $MDPATH"); } } sub exit_with { my $message = shift(); my $exitcode = shift(); print STDERR "$message Exiting...\n"; cleanup(); # fdasd isn't able to create volume label interactively # could be fixed with a reformat if ($exitcode == 65280) { $exitcode = 12; } # bigger exit codes are not supported if ($exitcode > 255) { $exitcode = 255; } exit($exitcode); } sub run_cmd { my $cmd = shift(); my $output = ""; if (! $OPT_DEBUG) { my ($app) = $cmd =~ /\/(\w+) /; # run command $output = `$cmd`; my $exit_code = $?; # wait for udev to finish processing system("$UDEVADM settle"); # only print output in case of an error or in verbose mode if ($output and ($exit_code != 0 or $OPT_VERBOSE)) { print("$output\n"); } if ($exit_code != 0) { exit_with("$app failed with exit code $exit_code", $exit_code); } } else { # only print the command in debug mode print("\`$cmd\`\n"); } return($output); } sub check_paths { for my $path ($BLKID, $PARTED, $FDASD, $DASDVIEW, $DASDFMT, $ZIPL, $UDEVADM, $ZGETDUMP) { unless ( -x $path) { exit_with("Command $path is not available.", 13); } } } sub read_file { my $path = shift(); open(my $file, "<", "$path") or exit_with("Unable to access $path: $!.", 15); my @content = <$file>; close($file); # no need for arrays in case of single lines if (@content > 1) { return @content; } else { chomp($content[0]); return($content[0]); } } sub is_dasd { # remove leading /dev/ my $device = substr(shift(), 5); if (-r "/sys/block/$device/device/discipline") { return(1); } else { return(0); } } sub has_free_single_kpartx { my $device = substr(shift(), 5); return(0) unless ($device =~ /^dm-[0-9]+$/); my $blockpath = "/sys/block/$device"; my @holders = glob("$blockpath/holders/*"); return(0) unless (@holders == 1); my $dmuuid = read_file("$holders[0]/dm/uuid"); return(0) unless ($dmuuid =~ /^part1-mpath-/); my @holderparts = split(/\//, $holders[0]); my $holder = "/dev/" . $holderparts[-1]; if(-b $holder and sysopen(my $blockdev, $holder, O_RDWR|O_EXCL)) { close($blockdev); return(1); } return(0); } sub is_zfcp { # remove leading /dev/ my $device = substr(shift(), 5); my $blockpath = "/sys/block/$device"; my $dmname = undef; # if user passed a device name on cmdline that we listed before # convert to a dm-[0-9]+ kernel device name if ($device =~ /^mapper\//) { $device = substr(readlink("/dev/" . $device), 3); $blockpath = "/sys/block/" . $device; } # check if dm-multipath and get one path member if ($device =~ /^dm-[0-9]+$/) { my $dmuuid = read_file("$blockpath/dm/uuid"); return(undef) unless $dmuuid =~ /^mpath-/; $dmname = read_file("$blockpath/dm/name"); opendir(DIR, "$blockpath/slaves/") or return(undef); while (defined(my $pathmember = readdir(DIR))) { # skip ".", "..", or other non scsi disk entries next unless $pathmember =~ /^sd[a-z]+$/; $device = $pathmember; last; } closedir(DIR); } my $devpath = "/sys/block/$device/device"; unless (-r "$devpath/hba_id" or -r "$devpath/type") { return(undef); } my $devtype = read_file("$devpath/type"); # SCSI type '0' means disk if ($devtype == 0) { if (defined $dmname) { return("/dev/mapper/$dmname"); } else { return("/dev/$device"); } } else { return(undef); } } sub get_partition_num { # remove leading /dev/ my $device = substr(shift, 5); my $part_num = grep(/\s+$device\d+/, read_file("/proc/partitions")); return($part_num); } sub print_device { my $device = shift(); my $only_dump_disks = shift(); my $devpath; if ($device =~ /^\/dev\/mapper\//) { $devpath = "/sys/block/" . substr(readlink($device), 3); } else { $devpath = "/sys/block/" . substr($device, 5); } my $output = $device; my $dump_device = 0; my $size = int(read_file("$devpath/size") / 2048); # 512 Byte blocks # size can't be read this way in case of unformatted devices if ($size != 0) { $output .= "\t${size}MB"; } else { $output .= "\tunknown"; } if (is_dasd($device)) { my ($busid) = readlink("$devpath/device") =~ /(\w\.\w\.\w{4})/; $output .= "\t$busid"; # check for dump record and list multi volumes my $zgetdump_output = `$ZGETDUMP -d $device 2>&1`; my @dump_devs = $zgetdump_output =~ /(\w\.\w\.\w{4})/g; if (@dump_devs) { $dump_device = 1; $output .= "\tdumpdevice"; # no need to output the dump ids for a single device if (@dump_devs > 1) { for my $id (@dump_devs) { $output .= "|$id"; } } } else { # check for single volume dump devices if ($zgetdump_output =~ /Single-volume DASD dump tool/) { $dump_device = 1; $output .= "\tdumpdevice"; } } } else { # get one path member to fill path info for "yast onpanic" if ($device =~ /^\/dev\/mapper\//) { my $blockdev = substr(readlink($device), 3); my $blockpath = "/sys/block/" . $blockdev; opendir(DIR, "$blockpath/slaves/") or return(undef); while (defined(my $pathmember = readdir(DIR))) { # skip ".", "..", or other non scsi disk entries next unless $pathmember =~ /^sd[a-z]+$/; $devpath = "/sys/block/" . $pathmember; last; } closedir(DIR); } my $adapter = read_file("$devpath/device/hba_id"); my $wwpn = read_file("$devpath/device/wwpn"); my $lun = read_file("$devpath/device/fcp_lun"); $output .= "\t$adapter\t$wwpn\t$lun"; # check for dump record my $zgetdump = `$ZGETDUMP -d $device 2>&1`; if ($? == 0) { my ($dsize) = ($zgetdump =~ /Maximum dump size\.:\s+([0-9]+) MB/m); $dsize = $size unless (defined($dsize)); $output = "$device\t${dsize}MB\t$adapter\t$wwpn\t$lun\tdumpdevice"; $dump_device = 1; } } if ($only_dump_disks) { if ($dump_device) { print("$output\n"); } } else { print("$output\n"); } } sub list_free_disks { my $devices_ref = shift(); my $type = shift(); if (@$devices_ref) { for my $device (@$devices_ref) { print_device($device); } } else { print STDERR "No free $type devices available!\n"; } } sub list_dump_disks { my @devices = @_; if (@devices) { for my $device (@devices) { print_device($device, 1); } } else { print STDERR "No dump devices available!\n"; } } sub determine_free_disks { my @dasd; my @zfcp; my @devices; # gather block devices my $path="/sys/block/"; opendir(DIR, $path) or exit_with("Unable to find $path: $!", 15); while (defined(my $file = readdir(DIR))) { # no need to add other devices then dasd* or sd* # or dm-multipath if ($file =~ /^dasd[a-z]+$/ or $file =~ /^sd[a-z]+$/ or $file =~ /^dm-[0-9]+$/) { push(@devices, $file); } } closedir(DIR); for my $entry (@devices) { # only allow disks, no partitions my ($device) = $entry =~ /^([a-z]+)$/; # dm devices other than dm-multipath are filtered by is_zfcp() ($device) = $entry =~ /^(dm-[0-9]+)$/ unless ($device); next unless ($device); $device = "/dev/$device"; # determine if the block device could be accessed exclusively if(-b $device and sysopen(my $blockdev, $device, O_RDWR|O_EXCL)) { close($blockdev); if (is_dasd($device)) { push(@dasd, $device); } my $zfcp = is_zfcp($device); if (defined $zfcp) { push(@zfcp, $zfcp); } } else { # A dm-multipath device with a single holder # being a kpartx partition number 1 could still # be free or contain a zfcpdump boot record. # Due to the kpartx linear dm mapping, such # dm-multipath device cannot open exclusively. if (has_free_single_kpartx($device)) { my $zfcp = is_zfcp($device); if (defined $zfcp) { push(@zfcp, $zfcp); } } } # wait for udev to process all events triggered by sysopen(,O_EXCL) system("$UDEVADM settle"); } return(\@dasd, \@zfcp); } sub prepare_dasd { my @devices = @_; my $format_disks = ""; # check formatting for my $device (@devices) { # determine disk layout my ($fmtstr) = `$DASDVIEW -x $device` =~ /(\w\w\w) formatted/; SWITCH: for($fmtstr) { if (/NOT/) { print("Unformatted disk, formatting $device.\n"); $format_disks .= " $device"; last SWITCH; } if (/LDL/) { if ($OPT_FORCE) { print("Linux disk layout, reformatting $device.\n"); $format_disks .= " $device"; } else { print("$device was formatted with the Linux disk layout.\n"); print("Unable to use it without reformatting.\n"); exit_with("Re-issue the mkdump command with the --force option.", 12); } last SWITCH; } if (/CDL/) { # allow reformatting with force, since fdasd isn't able to create volume label interactively if ($OPT_FORCE) { print("Compatible disk layout, force reformatting $device.\n"); $format_disks .= " $device"; } else { print("$device: Compatible disk layout, Ok to use.\n"); } last SWITCH; } exit_with("Unknown layout ($fmtstr), cannot use disk.", 11); } } # format devices if ($format_disks) { #up to eight devices in parallel run_cmd("$DASDFMT -P 8 -b 4096 -y -f $format_disks"); } # check partitioning and partition for my $device (@devices) { my $part_num = get_partition_num($device); if ($part_num == 0 or $OPT_FORCE) { print("Re-partitioning disk $device.\n"); run_cmd("$FDASD -a $device"); } else { # allow disk with one partition if it don't consist a file system if ($part_num == 1) { my ($fstype) = `$BLKID ${device}1` =~ /TYPE=\"(\w+)\"/; if ($fstype) { exit_with("Device ${device}1 already contains a filesystem of type $fstype.", 12); } } else { exit_with("$part_num partitions detected, cannot use disk $device.", 12); } } } } sub setup_dasddump { my @devices = @_; prepare_dasd(@devices); # create zipl device configuration file # don't create files in debug mode unless ($OPT_DEBUG) { open(my $file, ">", $MDPATH) or exit_with("Unable to access $MDPATH: $!.", 15); for my $device (@devices) { print{$file}("${device}1\n"); } close($file); } print("Creating dump record.\n"); run_cmd("${ZIPL} -V -n -M $MDPATH"); cleanup(); } sub setup_zfcpdump { my $device = shift(); # check partitioning my $part_num = get_partition_num($device); if ($part_num == 0 or $OPT_FORCE) { print("Re-partitioning disk $device.\n"); run_cmd("$PARTED -s -- $device mklabel gpt mkpart primary 0 -1"); } else { if ($part_num > 1) { exit_with("$part_num partitions detected, cannot use disk $device.", 12); } } # install bootloader print("Creating dump record.\n"); my $partdev; if ($device =~ /^\/dev\/mapper\//) { $partdev = $device . "-part1"; # kpartx partition on multipath } else { $partdev = $device . "1"; # real partition, single path SCSI } run_cmd("${ZIPL} -V -d ${partdev}"); cleanup(); } sub print_version { print << "EOF"; mkdump $VERSION Copyright (c) 2011 SUSE LINUX Products GmbH License GPLv2 or (at your option) any later version. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Written by Tim Hardeck . EOF exit(0); } sub print_usage { my $exitcode = shift(); print << "EOF"; Usage: mkdump [OPTIONS] [DEVICE]... mkdump $VERSION Prepare one or more volumes for use as S/390 dump device. Supported devices are ECKD DASD and SCSI over zFCP disks, while multi-volumes are limited to DASD. Only whole disks can be used, no partitions! If the device is incompatible formatted/partioned, the script will refuse to install the dump record unless the --force switch is given. Disks which are in use or have mounted partitions will not be listed and can't be used. The mentioning of "dumpdevice" after a disk indicates that it is an already usable dump device. Additionally multi-volume dump devices are indicated by the list of including DASD ids. Options: -h, --help display this help and exit -V, --version display version information and exit -d, --debug debug mode, do not run programs which commit changes -v, --verbose be verbose and show command outputs -f, --force force overwrite of the disk -l, --list-dump display dump disks -D, --list-dasd display usable DASD disks (Device, Size, ID, Dump) -Z, --list-zfcp display usable SCSI over zFCP disks (Device, Size, ID, WWPN, LUN, Dump) Report bugs on https://bugzilla.novell.com/ EOF exit($exitcode); } sub analyze_cmd_parameters { #verbose, debug and force are global my $opt_help = 0; my $opt_version = 0; my $opt_dump = 0; my $opt_dasd = 0; my $opt_zfcp = 0; if (@ARGV == 0) { print_usage(14); } Getopt::Long::Configure('bundling'); GetOptions( 'h|help' => \$opt_help, 'V|version' => \$opt_version, 'd|debug' => \$OPT_DEBUG, 'v|verbose' => \$OPT_VERBOSE, 'f|force' => \$OPT_FORCE, 'l|list-dump' => \$opt_dump, 'D|list-dasd' => \$opt_dasd, 'Z|list-zfcp' => \$opt_zfcp, ) or print_usage(14); if ($opt_help) { print_usage(0); } if ($opt_version) { print_version(); } # determine free dasd and zfcp devices my ($dasd_ref, $zfcp_ref) = determine_free_disks(); if ($opt_dump) { list_dump_disks(@$dasd_ref, @$zfcp_ref); exit 0; } if ($opt_dasd) { list_free_disks(\@$dasd_ref, "dasd"); } if ($opt_zfcp) { list_free_disks(\@$zfcp_ref, "zfcp"); } # allow listing of both device types at the same time if ($opt_dasd or $opt_zfcp) { exit 0; } # check provided devices and be strict my @devices; for my $device (@ARGV) { if (grep(/$device/, @devices)) { exit_with("$device is mentioned more than once.", 14); } # dm devices other than dm-multipath are filtered by is_zfcp() if ( $device =~ /^\/dev\/[a-z]+$/ == 0 and $device !~ /^\/dev\/mapper\// ) { exit_with("The device parameter $device is inaccurate. Only whole disks are allowed.", 14); } if (grep(/$device/, (@$dasd_ref, @$zfcp_ref))) { my $zfcp = is_zfcp($device); if (defined $zfcp and @ARGV > 1) { exit_with("Multi-volume dumps aren't supported with zFCP.", 14); } push(@devices, (defined $zfcp) ? $zfcp : $device); } else { if (-b $device) { exit_with("$device is in use or not a DASD/zFCP disk!", 14); } else { exit_with("$device does not exist!", 14); } } } if (@devices == 0) { exit_with("No usable devices where provided.", 14); } return(@devices); } sub main { check_paths(); my @devices = analyze_cmd_parameters(); # only one dump device is possible with zFCP which is enforced in analyze_cmd_parameters my $zfcp = is_zfcp($devices[0]); if (defined $zfcp) { setup_zfcpdump($zfcp); } else { setup_dasddump(@devices); } print("Creating the dump device was successful.\n"); } main();