#!/usr/bin/perl -T

#
# Copyright (C) 2019-2020 Red Hat, Inc. All rights reserved.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# 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

# Requires perl-JSON and util-linux version 2.35
#   util-linux source is available from https://github.com/karelzak/util-linux

###############################
use strict;
no locale;

use Carp;
use Cwd qw(realpath);
use Data::Dumper; # For debugging only
use File::Basename;
use File::Path qw(make_path remove_tree);
use File::Spec;
use File::Temp qw(tempfile tempdir);
use Getopt::Long qw(:config pass_through);
use JSON;
use Pod::Usage;
use POSIX qw(strftime pathconf);

####################
# We won't use PATH from environment
delete $ENV{PATH};

# Protect our temporary files and directories
umask 0077;

# Obtain command line options
my $debug = 0;
my $dryrun = 0;
my $file = "";
my $git = 0;
my $help = 0;
my $man = 0;
my $since = "";
my $until = "";
my $verbose = 0;

# Format is:
#   --debug
#   --dry-run
#   --file=filename.json
#   --git
#   --verbose
#   --since='2019-10-30 18:17:16'
#   --until='2019-10-30 18:17:16'

GetOptions("debug" => \$debug, "dry-run" => \$dryrun, "file=s" => \$file, "git" => \$git, "help|?" => \$help, "man" => \$man, "since=s" => \$since, "until=s" => \$until, "verbose" => \$verbose);

# Default locations of binaries
my $newlsblk = "/usr/bin/lsblk";
my $journalctl = "/usr/bin/journalctl";
my $systemd_analyze = "/usr/bin/systemd-analyze";
my $gitpath = "/usr/bin/git";

if ($debug) {
	# Allow binary path overrides
	$newlsblk = $1 if $ENV{LSBLK_PATH} =~ /(.+)/;
	$journalctl = $1 if $ENV{JOURNALCTL_PATH} =~ /(.+)/;
	$systemd_analyze = $1 if $ENV{SYSTEMD_ANALYZE_PATH} =~ /(.+)/;
	$gitpath = $1 if $ENV{GIT_PATH} =~ /(.+)/;

}

if ($debug || $git) {
	# Keep temporary files
	$File::Temp::KEEP_ALL = 1;
}

# Should we make extra entries in /sys for other data that didn't come from there?
my $extra = ($git ? 1 : 0);

pod2usage(0) if $help;
pod2usage(-exitstatus => 0, -verbose => 2) if $man;

$|=1 if ($verbose || $debug);

####################
# Arrays of device information

my %devname_by_devno;
my %holders;
my %slaves;

####################
# Enable all available security checks
File::Temp->safe_level(File::Temp::HIGH);

# Initialise a temporary directory
my $tempname = ($git ? "udev_git" : "lsblk_wrap");
my $basedir = tempdir($tempname."_XXXXXXXX", TMPDIR => 1, CLEANUP => 1);
my $path_max = POSIX::pathconf($basedir, &POSIX::_PC_PATH_MAX) - 1;

print "Using temporary directory: $basedir\n" if $verbose;

####################
# File management subroutines that report what they do when --verbose is used

sub create_path(@)
{
	foreach my $path (@_) {
		# Reserve 128 characters for filename inside directory
		die "Directory name exceeds limit of " . (${path_max} - 128) . " characters: $path" if (length $path > $path_max - 128);
		next if -d $path;
		print "  Creating directory $path\n" if $verbose;
		make_path($path) if !$dryrun;
	}
};

sub create_file($$)
{
	my $filename = shift;
	my $contents = shift;

	return if !defined $contents;

	# Strip quotes from some captured data - FIXME seek a way to avoid this
	$contents = $1 if $contents =~ /'(.*)'/;

	return if !length $contents;

	die "Filename exceeds limit of " . ${path_max} . " characters: $filename" if (length $filename > $path_max);

	print "  Creating file $filename\n" if $verbose;
	return if $dryrun;

	create_path(dirname($filename));

	my ($fd, $tmpfile) = tempfile(basename($filename)."_XXXXXXXX", SUFFIX => ".tmp", DIR => $basedir);
	rename($tmpfile, $filename) or croak "Failed to rename $tmpfile to $filename";
	File::Temp::cmpstat($fd, $filename) or croak "$filename got changed externally";
	print $fd $contents."\n" if defined($contents);
	close $fd;
};

sub create_symlink($$)
{
	my $dest = shift;
	my $link = shift;

	my ($realdest) = (realpath($dest) =~ /^(.*)$/);

	if ($dryrun) {
		print "  Creating symlink from $link to $dest\n" if $verbose;
		return;
	}

	my $rellink = File::Spec->abs2rel(($realdest), realpath(dirname($link)));

	if (symlink($rellink, $link)) {
		print "  Created symlink from $link to $realdest as $rellink\n" if $verbose;
		return;
	}
	if (!$!{EEXIST}) {
		carp "Failed to create symlink from $link to $realdest as $rellink";
		return;
	}

	return if (-l $link && (readlink $link == $rellink));

	# If it already exists, replace it
	# FIXME Only do this if it changed
	if (!unlink($link)) {
		carp "Failed to remove $link for replacement: $!";
		return;
	}
	if (!symlink($rellink, $link)) {
		carp "Failed to update symlink from $link to $realdest as $rellink";
		return;
	}
	print "  Updated symlink from $link to $realdest as $rellink\n" if $verbose;
};

sub remove_file($)
{
	my $file = shift;

	print "  Removing $file\n" if $verbose;
	return if $dryrun;
	unlink $file;
};

####################

# Use external systemd-analyze to process --since and --until time specifications
sub convert_timestamp($)
{
	my $timespec = shift;
	my $timestamp = 0;

	my @newargs = ("systemd-analyze", "timestamp");
	push @newargs, "$1" if ($timespec =~ /(.+)/);

	print "Running: ".join(" ", @newargs)."\n" if $verbose;
	die "fork failed: $!" unless defined(my $pid = open(TIMESTAMP, "-|"));
	if (!$pid) {           # child process
		(system { $systemd_analyze } @newargs) == 0 or croak "Failed to run $systemd_analyze";
		exit 1;
	}

	# External format is:
	#   UNIX seconds: @1581106829.870249    
	# Internally we use the journal format that drops the decimal point.
	while (<TIMESTAMP>) {
		$timestamp = $1.$2."0" x (6-length($2)) if /UNIX seconds: @([0-9]+)?(?:\.([0-9]+))*/;
	}

	close TIMESTAMP;

	$timestamp;
};

####################

my $gitdir = "";

# FIXME Deal with --dryrun!
sub git_command(@)
{
	my @newargs = ("git");

	push @newargs, "--git-dir", $gitdir, "--work-tree", $basedir if ($gitdir =~ /(.+)/);
	push @newargs, @_;
	
	print "Running: ".join(" ", @newargs)."\n" if $verbose;
	(system { $gitpath } @newargs) == 0 or croak "Failed to run $gitpath";
};

####################

my $sysdir = $basedir."/sys";
my $devdir = $basedir."/dev";

create_path($devdir, $sysdir."/block", $sysdir."/dev/block");

# FIXME Add mountpoints when we can record them.
# create_file($basedir."/proc/self/mountinfo", undef);

####################

if ($git) {
	git_command("init", "-q", $basedir);
	$gitdir = $basedir."/.git";
}

####################

# Read journal entries:  

# Set up LOG as STDIN or $file or output from journalctl.
if ($file eq "-") {
	print "Reading JSON from standard input.\n" if $verbose;
	*LOG = *STDIN;
} elsif ($file ne "") {
	print "Reading JSON from $file.\n" if $verbose;
	open(LOG, '<', $file) or die "Open $file failed: $!";
} else {
	# journalctl -t UDEVLOG -b --output json SUBSYSTEM=block

	my @newargs = ("journalctl", "-t", "UDEVLOG", "-b", "--output", "json", "SUBSYSTEM=block");

	# Pass arguments --since and --until to external journalctl command
	push @newargs, "--since=$1" if ($since =~ /(.+)/);
	push @newargs, "--until=$1" if ($until =~ /(.+)/);

	print "Running: ".join(" ", @newargs)."\n" if $verbose;
	die "fork failed: $!" unless defined(my $pid = open(LOG, "-|"));
	if (!$pid) {           # child process
		(system { $journalctl } @newargs) == 0 or croak "Failed to run $journalctl";
		exit 1;
	}
}

# Process all input lines by default
my $process_line = 1;

# If we're processing a file or standard input, we must filter with --since and --until internally.
my $since_timestamp = 0;
my $until_timestamp = 0;

if ($file ne "") {
	if ($since ne "") {
		# Don't process lines until we reach $since
		$process_line = 0;

		$since_timestamp = convert_timestamp($since);
		
		print "Processing records from realtime $since_timestamp.\n" if $verbose;
	}
	if ($until ne "") {
		$until_timestamp = convert_timestamp($until);

		print "Processing records until realtime $until_timestamp.\n" if $verbose;
	}
}

while (<LOG>) {
	my $logentry = decode_json $_;

	if ($logentry->{SYSLOG_IDENTIFIER} ne "UDEVLOG") {
		print "Ignoring line $. - Not a udev message.\n" if $verbose;
		next;
	};
	if ($logentry->{SUBSYSTEM} ne "block") {
		print "Ignoring line $. - Not a udev block device message.\n" if $verbose;
		next;
	};

	if (!$process_line && $since_timestamp > 0 && $logentry->{_SOURCE_REALTIME_TIMESTAMP} >= $since_timestamp) {
		print "Reached timestamp $since_timestamp - Starting processing.\n" if $verbose;
		$process_line = 1;
	}

	if ($process_line && $until_timestamp > 0 && $logentry->{_SOURCE_REALTIME_TIMESTAMP} > $until_timestamp) {
		print "Reached timestamp $until_timestamp - Stopping processing.\n" if $verbose;
		$process_line = 0;
	}

	if (!$process_line) {
		print "Ignoring line $. - Timestamp $logentry->{_SOURCE_REALTIME_TIMESTAMP} outside range.\n" if $verbose;
		next;
	}

	my $action = "";
	$action = $1 if $logentry->{ACTION} =~ /([a-zA-Z]+)/;
	
	print "Processing line $. (".strftime("%Y/%m/%d %T.", localtime $logentry->{_SOURCE_REALTIME_TIMESTAMP} / 1000000). ($logentry->{_SOURCE_REALTIME_TIMESTAMP} % 1000000).") - $action $logentry->{DEVNAME}\n" if $verbose;

	my $majmin = $logentry->{MAJOR}.":".$logentry->{MINOR};
	if ($majmin =~ /^(\d+:\d+)$/) {
		$majmin = $1;
	} else {
		carp "Ignoring log entry: Invalid MAJOR:MINOR $majmin";
		next;
	}

	my $devname = "";
	if ($logentry->{DEVNAME} =~ /^(\/dev\/[-a-zA-Z0-9#+:=\@_][-a-zA-Z0-9#+.:=\@_]*)$/) {
		$devname = $1;
	} else {
		carp "Ignoring log entry: Invalid DEVNAME $devname for $majmin";
		next;
	}

	my $devpath;
	# Limit to character set supported by udev
	if ($logentry->{DEVPATH} !~ /(\.\.|\/\.\.|\.\.\/)/ && $logentry->{DEVPATH} =~ /^([-a-zA-Z0-9#+.:=\@_\/\\]*)$/) {
		$devpath = $1;
	} else {
		carp "Ignoring log entry: Invalid DEVPATH value $logentry->{DEVPATH} for $majmin";
		next;
	}

	if ($action eq "change" || $action eq "add") {
		create_file("${basedir}${devname}",
			"MAJOR=$logentry->{MAJOR}\n".
			"MINOR=$logentry->{MINOR}\n".
			"OWNER=$logentry->{UDEVLOG_LSBLK_OWNER}\n".
			"GROUP=$logentry->{UDEVLOG_LSBLK_GROUP}\n".
			"MODE=$logentry->{UDEVLOG_LSBLK_MODE}\n".
			"ID_FS_LABEL_ENC=$logentry->{ID_FS_LABEL_ENC}\n".
			"ID_FS_UUID_ENC=$logentry->{ID_FS_UUID_ENC}\n".
			"ID_PART_ENTRY_NAME=$logentry->{ID_PART_ENTRY_NAME}\n".
			"ID_PART_TABLE_UUID=$logentry->{ID_PART_TABLE_UUID}\n".
			"ID_PART_TABLE_TYPE=$logentry->{ID_PART_TABLE_TYPE}\n".
			"ID_FS_TYPE=$logentry->{ID_FS_TYPE}\n".
			"ID_PART_ENTRY_TYPE=$logentry->{ID_PART_ENTRY_TYPE}\n".
			"ID_PART_ENTRY_UUID=$logentry->{ID_PART_ENTRY_UUID}\n".
			"ID_PART_ENTRY_FLAGS=$logentry->{ID_PART_ENTRY_FLAGS}\n".
			"ID_MODEL=$logentry->{ID_MODEL}\n".
			"ID_WWN_WITH_EXTENSION=$logentry->{ID_WWN_WITH_EXTENSION}\n".
			"ID_WWN=$logentry->{ID_WWN}\n".
			"ID_SCSI_SERIAL=$logentry->{ID_SCSI_SERIAL}\n".
			"ID_SERIAL_SHORT=$logentry->{ID_SERIAL_SHORT}");

		foreach my $devlink (split (/ /, $logentry->{DEVLINKS})) {
			if ($devlink !~ /(\.\.|\/\.\.|\.\.\/)/ && $devlink =~ /^([-a-zA-Z0-9#+.:=\@_\/\\]+)$/) {
				$devlink = $1;
			} else {
				carp "Skipping invalid DEVLINKS value $devlink";
				next;
			}

			create_path($basedir.dirname($devlink));
			create_symlink($basedir.$devname, $basedir.$devlink);
		};

		my $sysdevdir = $sysdir.$devpath;
		create_path($sysdevdir."/holders");

		if ($logentry->{DEVTYPE} eq "disk") {
			create_symlink($sysdir.$devpath, $sysdir."/block/".basename($devname));

			create_path($sysdevdir."/slaves", $sysdevdir."/queue", $sysdevdir."/device");

			create_file("$sysdevdir/device/model", $logentry->{UDEVLOG_LSBLK_MODEL});
			create_file("$sysdevdir/device/state", $logentry->{UDEVLOG_LSBLK_STATE});

			create_file("$sysdevdir/device/type", $logentry->{UDEVLOG_DEVICE_TYPE});
			create_file("$sysdevdir/device/rev", $logentry->{UDEVLOG_DEVICE_REV});
			create_file("$sysdevdir/device/vendor", $logentry->{UDEVLOG_DEVICE_VENDOR});
			create_file("$sysdevdir/device/serial", $logentry->{UDEVLOG_DEVICE_SERIAL});
		};

		create_symlink($sysdir.$devpath, $sysdir."/dev/block/".$majmin);

		create_file("$sysdevdir/dev", $majmin);
		create_file("$sysdevdir/ro", $logentry->{UDEVLOG_BLOCK_RO});
		create_file("$sysdevdir/alignment_offset", $logentry->{UDEVLOG_BLOCK_ALIGNMENT_OFFSET});
		create_file("$sysdevdir/discard_alignment", $logentry->{UDEVLOG_BLOCK_DISCARD_ALIGNMENT});
		create_file("$sysdevdir/ext_range", $logentry->{UDEVLOG_BLOCK_EXT_RANGE});
		create_file("$sysdevdir/capability", $logentry->{UDEVLOG_BLOCK_CAPABILITY});
		create_file("$sysdevdir/size", $logentry->{UDEVLOG_BLOCK_SIZE});
		create_file("$sysdevdir/removable", $logentry->{UDEVLOG_BLOCK_REMOVABLE});

		# Device-mapper entries
		if ($devname =~ /^\/dev\/dm-/) {
			create_file("$sysdevdir/dm/name", $logentry->{DM_NAME});
			create_file("$sysdevdir/dm/uuid", $logentry->{DM_UUID});
			create_file("$sysdevdir/dm/suspended", $logentry->{DM_SUSPENDED});

			if ($extra) {
				create_file("$sysdevdir/dm/EXTRA/read_ahead", $logentry->{UDEVLOG_DM_READ_AHEAD});
				create_file("$sysdevdir/dm/EXTRA/attr", $logentry->{UDEVLOG_DM_ATTR});
				create_file("$sysdevdir/dm/EXTRA/tables_loaded", $logentry->{UDEVLOG_DM_TABLES_LOADED});
				create_file("$sysdevdir/dm/EXTRA/readonly", $logentry->{UDEVLOG_DM_READONLY});
				create_file("$sysdevdir/dm/EXTRA/open", $logentry->{UDEVLOG_DM_OPEN});
				create_file("$sysdevdir/dm/EXTRA/segments", $logentry->{UDEVLOG_DM_SEGMENTS});
				create_file("$sysdevdir/dm/EXTRA/events", $logentry->{UDEVLOG_DM_EVENTS});
				create_file("$sysdevdir/dm/EXTRA/device_count", $logentry->{UDEVLOG_DM_DEVICE_COUNT});
				create_file("$sysdevdir/dm/EXTRA/devs_used", $logentry->{UDEVLOG_DM_DEVS_USED});
				create_file("$sysdevdir/dm/EXTRA/devnos_used", $logentry->{UDEVLOG_DM_DEVNOS_USED});
				create_file("$sysdevdir/dm/EXTRA/blkdevs_used", $logentry->{UDEVLOG_DM_BLKDEVS_USED});
				create_file("$sysdevdir/dm/EXTRA/device_ref_count", $logentry->{UDEVLOG_DM_DEVICE_REF_COUNT});
				create_file("$sysdevdir/dm/EXTRA/names_using_dev", $logentry->{UDEVLOG_DM_NAMES_USING_DEV});
				create_file("$sysdevdir/dm/EXTRA/devnos_using_dev", $logentry->{UDEVLOG_DM_DEVNOS_USING_DEV});
				create_file("$sysdevdir/dm/EXTRA/subsystem", $logentry->{UDEVLOG_DM_SUBSYSTEM});
				create_file("$sysdevdir/dm/EXTRA/lv_layer", $logentry->{UDEVLOG_LV_LAYER});
				create_file("$sysdevdir/dm/EXTRA/table_live", $logentry->{UDEVLOG_DM_TABLE_LIVE});
				create_file("$sysdevdir/dm/EXTRA/table_inactive", $logentry->{UDEVLOG_DM_TABLE_INACTIVE});
				create_file("$sysdevdir/dm/EXTRA/vg_name", $logentry->{DM_VG_NAME});
				create_file("$sysdevdir/dm/EXTRA/lv_name", $logentry->{DM_LV_NAME});
				create_file("$sysdevdir/dm/EXTRA/lv_layer", $logentry->{DM_LV_LAYER});
				create_file("$sysdevdir/dm/EXTRA/low_priority_flag", $logentry->{DM_UDEV_LOW_PRIORITY_FLAG});
			};
		};

		if ($logentry->{DEVTYPE} eq "disk") {
			create_file("$sysdevdir/queue/discard_max_bytes", $logentry->{UDEVLOG_QUEUE_DISCARD_MAX_BYTES});
			create_file("$sysdevdir/queue/scheduler", $logentry->{UDEVLOG_QUEUE_SCHEDULER});
			create_file("$sysdevdir/queue/nr_requests", $logentry->{UDEVLOG_QUEUE_NR_REQUESTS});
			create_file("$sysdevdir/queue/minimum_io_size", $logentry->{UDEVLOG_QUEUE_MINIMUM_IO_SIZE});
			create_file("$sysdevdir/queue/discard_zeroes_data", $logentry->{UDEVLOG_QUEUE_DISCARD_ZEROES_DATA});
			create_file("$sysdevdir/queue/rotational", $logentry->{UDEVLOG_QUEUE_ROTATIONAL});
			create_file("$sysdevdir/queue/read_ahead_kb", $logentry->{UDEVLOG_QUEUE_READ_AHEAD_KB});
			create_file("$sysdevdir/queue/physical_block_size", $logentry->{UDEVLOG_QUEUE_PHYSICAL_BLOCK_SIZE});
			create_file("$sysdevdir/queue/optimal_io_size", $logentry->{UDEVLOG_QUEUE_OPTIMAL_IO_SIZE});
			create_file("$sysdevdir/queue/logical_block_size", $logentry->{UDEVLOG_QUEUE_LOGICAL_BLOCK_SIZE});
			create_file("$sysdevdir/queue/discard_granularity", $logentry->{UDEVLOG_QUEUE_DISCARD_GRANULARITY});
		}

		create_file("$sysdevdir/queue/add_random", $logentry->{UDEVLOG_QUEUE_ADD_RANDOM});
		create_file("$sysdevdir/queue/write_same_max_bytes", $logentry->{UDEVLOG_QUEUE_WRITE_SAME_MAX_BYTES});
		create_file("$sysdevdir/queue/zoned", $logentry->{UDEVLOG_QUEUE_ZONED});
		create_file("$sysdevdir/md/level", $logentry->{MD_LEVEL});

		# Various extra fields with nowhere else to go
		if ($extra) {
			create_file("$sysdevdir/EXTRA/label", $logentry->{UDEVLOG_LSBLK_LABEL});
			create_file("$sysdevdir/EXTRA/mountpoint", $logentry->{UDEVLOG_LSBLK_MOUNTPOINT});

			create_file("$sysdevdir/EXTRA/nvme/firmware_rev", $logentry->{UDEVLOG_NVME_FIRMWARE_REV});
			create_file("$sysdevdir/EXTRA/nvme/model", $logentry->{UDEVLOG_NVME_MODEL});
			create_file("$sysdevdir/EXTRA/nvme/state", $logentry->{UDEVLOG_NVME_STATE});

			create_file("$sysdevdir/EXTRA/fc_host/port_id", $logentry->{UDEVLOG_FC_PORT_ID});
			create_file("$sysdevdir/EXTRA/fc_host/port_name", $logentry->{UDEVLOG_FC_PORT_NAME});
			create_file("$sysdevdir/EXTRA/fc_host/port_state", $logentry->{UDEVLOG_FC_PORT_STATE});
			create_file("$sysdevdir/EXTRA/fc_host/fabric_name", $logentry->{UDEVLOG_FC_FABRIC_NAME});
			create_file("$sysdevdir/EXTRA/fc_host/node_name", $logentry->{UDEVLOG_FC_NODE_NAME});
			create_file("$sysdevdir/EXTRA/fc_host/speed", $logentry->{UDEVLOG_FC_SPEED});
			create_file("$sysdevdir/EXTRA/fc_host/port_type", $logentry->{UDEVLOG_FC_PORT_TYPE});
			create_file("$sysdevdir/EXTRA/fc_host/symbolic_name", $logentry->{UDEVLOG_FC_SYMBOLIC_NAME});
			create_file("$sysdevdir/EXTRA/fc_host/tgtid_bind_type", $logentry->{UDEVLOG_FC_TGTID_BIND_TYPE});

			create_file("$sysdevdir/EXTRA/scsi_host/driver_version", $logentry->{UDEVLOG_SCSI_DRIVER_VERSION});
			create_file("$sysdevdir/EXTRA/scsi_host/fw_state", $logentry->{UDEVLOG_SCSI_FW_STATE});
			create_file("$sysdevdir/EXTRA/scsi_host/fw_version", $logentry->{UDEVLOG_SCSI_FW_VERSION});
			create_file("$sysdevdir/EXTRA/scsi_host/model_desc", $logentry->{UDEVLOG_SCSI_MODEL_DESC});
			create_file("$sysdevdir/EXTRA/scsi_host/model_name", $logentry->{UDEVLOG_SCSI_MODEL_NAME});
			create_file("$sysdevdir/EXTRA/scsi_host/serial_num", $logentry->{UDEVLOG_SCSI_SERIAL_NUM});
			create_file("$sysdevdir/EXTRA/scsi_host/state", $logentry->{UDEVLOG_SCSI_STATE});
			create_file("$sysdevdir/EXTRA/scsi_host/active_mode", $logentry->{UDEVLOG_SCSI_ACTIVE_MODE});
		};

		# Store holders and slaves for the end
		($devname_by_devno{$majmin}) = $devname =~ /\/dev\/(.*)/;
		$holders{$majmin} = $logentry->{UDEVLOG_BLOCK_HOLDERS};
		$slaves{$majmin} = $logentry->{UDEVLOG_BLOCK_SLAVES};

	} elsif ($action eq "remove") {
		# Remove this item.  What about dependencies - can we trust the ordering?
		remove_file "${basedir}${devname}";

		foreach my $devlink (split (/ /, $logentry->{DEVLINKS})) {
			if ($devlink !~ /(\.\.|\/\.\.|\.\.\/)/ && $devlink =~ /^([-a-zA-Z0-9#+.:=\@_\/\\]+)$/) {
				$devlink = $1;
			} else {
				carp "Not trying to remove invalid DEVLINKS value $devlink for $devname ($majmin)";
				next;
			}

			remove_file $basedir.$devlink;
		};

		remove_file $sysdir."/block/".basename($devname);
		remove_file $sysdir."/dev/block/".$majmin;

		print "  Removing directory $sysdir$devpath\n" if $verbose;
		remove_tree($sysdir.$devpath) if !$dryrun;

		# Remove stored holders and slaves
		delete $devname_by_devno{$majmin};
		delete $holders{$majmin};
		delete $slaves{$majmin};
	};

	next if !$git;

	# FIXME In git mode also fix up holders/slaves after appropriate events (some must be paired up)

	# Add/remove all changed files
	git_command("add", "-A");

	# Create a git commit.
	# FIXME Eliminate empty commits.
	my $datestr = $1 if (strftime("%Y/%m/%d %H:%M:%S.", localtime $logentry->{_SOURCE_REALTIME_TIMESTAMP} / 1000000).($logentry->{_SOURCE_REALTIME_TIMESTAMP} % 1000001)) =~ /(.+)/;
	my $commit_msg = "${action} ${devname} ($.)\n\n${datestr}";
	git_command("commit", "-q", "-m", "$commit_msg", "--allow-empty");

	my $tag = $1 if (strftime("d_%Y%m%d_%H%M%S.", localtime $logentry->{_SOURCE_REALTIME_TIMESTAMP} / 1000000).($logentry->{_SOURCE_REALTIME_TIMESTAMP} % 1000001)) =~ /(.+)/;
	git_command("tag", $tag);
}

close LOG;

####################
# Nothing more to do in git mode
print "Git repository created in $basedir\n" if $git;
exit 0 if $git;

####################
print "Processing all device holders\n" if $verbose;

# Create holders/slaves symlinks
foreach my $majmin (keys %holders) {
	foreach my $holder_majmin (split / /, $holders{$majmin}) {
		if ($holder_majmin =~ /^(\d+:\d+)$/) {
			$holder_majmin = $1;
		} else {
			carp "Ignoring invalid holder MAJOR:MINOR $holder_majmin for $majmin";
			next;
		}
		create_symlink($sysdir."/dev/block/$holder_majmin", $sysdir."/dev/block/".$majmin."/holders/".$devname_by_devno{$holder_majmin});
		create_symlink($sysdir."/dev/block/".$majmin, $sysdir."/dev/block/".$holder_majmin."/slaves/".$devname_by_devno{$majmin});
	}
};

print "Processing all device slaves\n" if $verbose;

foreach my $majmin (keys %slaves) {
	foreach my $slave_majmin (split / /, $slaves{$majmin}) {
		if ($slave_majmin =~ /^(\d+:\d+)$/) {
			$slave_majmin = $1;
		} else {
			carp "Ignoring invalid slave MAJOR:MINOR $slave_majmin for $majmin";
			next;
		}
		create_symlink($sysdir."/dev/block/".$slave_majmin, $sysdir."/dev/block/".$majmin."/slaves/".$devname_by_devno{$slave_majmin});
		create_symlink($sysdir."/dev/block/".$majmin, $sysdir."/dev/block/".$slave_majmin."/holders/".$devname_by_devno{$majmin});
	}
};

####################
# Finally run 'lsblk' using the specially-prepared context

my @newargs = (basename($newlsblk), "--sysroot=".$basedir);

foreach my $arg (@ARGV) {
	push @newargs, $arg =~ /^(.*)$/;
}

print "Running: ".join(" ", @newargs)."\n" if $verbose;
if (!$dryrun) {
	(system { $newlsblk } @newargs) == 0 or croak "Failed to run $newlsblk";
}

exit 0;

# FIXME Check this list of remaining variables found in example udev
# database exports for any more that are worth extracting into files.
#
# ABS
# BUSNUM
# DEVNUM
# DM_ACTIVATION
# DM_MULTIPATH_DEVICE_PATH
# DM_MULTIPATH_TIMESTAMP
# DM_MULTIPATH_WIPE_PARTS
# DM_SUBSYSTEM_UDEV_FLAG0
# DM_UDEV_DISABLE_DISK_RULES_FLAG
# DM_UDEV_DISABLE_LIBRARY_FALLBACK_FLAG
# DM_UDEV_DISABLE_OTHER_RULES_FLAG
# DM_UDEV_DISABLE_SUBSYSTEM_RULES_FLAG
# DM_UDEV_PRIMARY_SOURCE_FLAG
# DM_UDEV_RULES_VSN
# DRIVER
# EV
# FWUPD_GUID
# HID_ID
# HID_NAME
# HID_PHYS
# HID_UNIQ
# ID_ATA
# ID_ATA_FEATURE_SET_PM
# ID_ATA_FEATURE_SET_PM_ENABLED
# ID_ATA_SATA
# ID_ATA_SATA_SIGNAL_RATE_GEN1
# ID_BUS
# ID_CDROM
# ID_CDROM_CD
# ID_CDROM_CD_R
# ID_CDROM_CD_RW
# ID_CDROM_DVD
# ID_CDROM_DVD_PLUS_R
# ID_CDROM_DVD_PLUS_R_DL
# ID_CDROM_DVD_PLUS_RW
# ID_CDROM_DVD_R
# ID_CDROM_DVD_RAM
# ID_CDROM_DVD_RW
# ID_CDROM_MRW
# ID_CDROM_MRW_W
# ID_FOR_SEAT
# ID_FS_USAGE
# ID_FS_UUID
# ID_FS_VERSION
# ID_INPUT
# ID_INPUT_KEY
# ID_INPUT_KEYBOARD
# ID_INPUT_MOUSE
# ID_MM_CANDIDATE
# ID_MODEL_ENC
# ID_MODEL_FROM_DATABASE
# ID_MODEL_ID
# ID_NET_DRIVER
# ID_NET_LABEL_ONBOARD
# ID_NET_LINK_FILE
# ID_NET_NAME
# ID_NET_NAME_MAC
# ID_NET_NAME_ONBOARD
# ID_NET_NAME_PATH
# ID_OUI_FROM_DATABASE
# ID_PART_ENTRY_DISK
# ID_PART_ENTRY_NUMBER
# ID_PART_ENTRY_OFFSET
# ID_PART_ENTRY_SCHEME
# ID_PART_ENTRY_SIZE
# ID_PATH
# ID_PATH_TAG
# ID_PCI_CLASS_FROM_DATABASE
# ID_PCI_INTERFACE_FROM_DATABASE
# ID_PCI_SUBCLASS_FROM_DATABASE
# ID_REVISION
# ID_SCSI
# ID_SERIAL
# ID_TARGET_PORT
# ID_TYPE
# ID_USB_CLASS_FROM_DATABASE
# ID_USB_DRIVER
# ID_USB_INTERFACE_NUM
# ID_USB_INTERFACES
# ID_USB_PROTOCOL_FROM_DATABASE
# ID_VENDOR
# ID_VENDOR_ENC
# ID_VENDOR_FROM_DATABASE
# ID_VENDOR_ID
# ID_WWN_VENDOR_EXTENSION
# IFINDEX
# INTERFACE
# KEY
# LED
# LIBINPUT_DEVICE_GROUP
# MODALIAS
# MPATH_SBIN_PATH
# MSC
# NAME
# PARTN
# PCI_CLASS
# PCI_ID
# PCI_SLOT_NAME
# PCI_SUBSYS_ID
# PHYS
# PRODUCT
# PROP
# REL
# SND
# SYSTEMD_ALIAS
# SYSTEMD_MOUNT_DEVICE_BOUND
# SYSTEMD_READY
# SYSTEMD_WANTS
# TAGS
# TYPE
# UDEV_BIOSDEVNAME
# UNIQ
# USEC_INITIALIZED
# WMI_GUID
 
__END__

=head1 NAME

lsblkj - Report logged historic data using lsblk

=head1 SYNOPSIS

lsblkj [options] [lsblk options] [device...]

=head1 OPTIONS

=over 8

=item B<--debug>

Allow the environment variables LSBLK_PATH and JOURNALCTL_PATH to
override the hard-coded locations of the external lsblk and journalctl
binaries.  Do not delete the temporary directories and files when the
script exits.

=item B<--dry-run>

Do not create the temporary files and directories the script requires
and do not call lsblk.
Useful with --verbose to check the effect of your input data.

=item B<--file=>

Read pre-filtered input from the named file instead of invoking journalctl 
directly.  Use - to read from STDIN.

=item B<--git>

Create a git repository instead of running lsblk.  Device information is
stored in files in sub-directories modelled on /dev and /sys.  Each log
record is translated into a commit and tagged with a timestamp to make
it easier to examine the changes over time. 

=item B<--help>

Print a brief help message and exit.

=item B<--man>

Print the manual page and exit.

=item B<--since=>

Timestamp of the first journal record to use when reconstructing the system's
device configuration.  This will normally be a time when the machine was booted.
The basic time format is "YYYY-MM-DD HH:II:SS" but for more details refer
to the man pages for journalctl(1) and systemd.time(7).

=item B<--until=>

Timestamp of the last journal record to use when reconstructing the system's
device configuration.  Storage logging should have been running continuously
and the system should not have been rebooted between the time specified
with the --since argument and this time. 
The basic time format is "YYYY-MM-DD HH:II:SS" but for more details refer
to the man pages for journalctl(1) and systemd.time(7).

=item B<--verbose>

Report the actions of the wrapper.

=back

=head1 DESCRIPTION

B<lsblkj> reproduces the system's storage block device configuration at
a specified earlier time then uses lsblk to show you the result.  
You may use any of lsblk's options, but filesystem and mount point
information will rarely be accurate because the data is only sampled
when there is a change to the underlying block device.

The raw data can also be accessed directly using journalctl(1).
For example, to find the major and minor number of the LVM Logical
Volume vg1/lvol0 across multiple reboot cycles you could use:

journalctl -t UDEVLOG --output verbose --output-fields=PERSISTENT_STORAGE_ID,MAJOR,MINOR PERSISTENT_STORAGE_ID=dm-name-vg1-lvol0

Additional fields that lsblk does not show, such as device-mapper
tables (UDEVLOG_DM_TABLE_LIVE), can be retrieved using this method.

=head1 SEE ALSO

journalctl(1), 
systemd.time(7),
lsblk(8)

=head1 AUTHOR

Alasdair G. Kergon <agk@redhat.com>

Please send bug reports and suggestions to the linux-lvm mailing list linux-lvm@redhat.com 
https://www.redhat.com/mailman/listinfo/linux-lvm 
or use the web interface at https://github.com/lvmteam/storage-logger

=cut
