#!/usr/bin/env perl
#
## luks_mounter.pl
##
## This is a script used to dynamically mount external USB HDD's, some of which
## are encrypted using LUKS. This makes the devices less than easy to determine
## what they are and where they should be mounted. An example use case for this
## script (my current use case) is an offsite backup rotation where you have
## one or more drives rotating. This script was developed to ensure the drives
## could be plugged in, in any order and be uniquely identified based on a
## specific formatting detailed here:
## kissitconsulting.com/blog/post/a-slick-option-for-dynamically-mounting-luks-encrypted-external-hard-drives
##
## However, a quick example here also. Lets say you have a HDD that you want
## to encrypt. Create two partitions, the first being a standard unencrypted
## partition that you create a filesystem label for when you create the filesystem.
## This device will then show up as /dev/disk/by-label/label. Create a 2nd partition
## and configure it as a LUKS device accessible by a key file. Create your filesystem
## of choice on this device. Then configure this script to mount it based on the
## label of the first partition.
## Copyright (C) 2016 KISS IT Consulting
## 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 ANY
## 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.
##
## Instructions
## 1. Install required CPAN modules, on CentOS 6.x:
## yum install perl-MIME-Lite perl-Config-Simple
## 2. Configure your mounts/devices as needed in the ini file. Documentation and examples are provided in the example file.
## 3. Run the script as root passing your desired options
##
use strict;
use MIME::Lite;
use Config::Simple;
use File::Basename;
use Getopt::Long;
## Get our command line options
my $dirname = dirname(__FILE__);
my @status_msg;
my $status = 0;
my $help = 0;
my $clean = 0;
my $do_mount = 0;
my $do_unmount = 0;
my $state = '';
my $config = "$dirname/luks_mounter.ini";
GetOptions("help" => \$help, "clean" => \$clean, "mount" => \$do_mount, "unmount" => \$do_unmount, "config=s" => \$config);
if ($help) {
print <error();
my $key_file = $cfg->param("base.key_file");
my $set_user = $cfg->param("base.set_user");
my $set_group = $cfg->param("base.set_group");
my @mount_tasks = $cfg->param("base.mount_tasks");
my @unmount_tasks = $cfg->param("base.unmount_tasks");
## Before going any further, validate that we have mounts configured
my @mounts = $cfg->param("base.mounts");
if(!@mounts) {
die "No mounts configured, nothing to do."
}
## Clear a previous error log if one exists
my $error_log = $cfg->param("base.error_log");
if(-e $error_log) {
run_system_cmd("rm $error_log");
}
## Run any pre tasks we may have
## Force a refresh of the disk devices (only if we are mounting)
if ($do_mount == 1) {
my $device_refresh_wait = $cfg->param("base.device_refresh_wait");
run_system_cmd("partprobe > /dev/null 2>&1");
push(@status_msg, "Refreshed disk devices using partprobe");
if (length($device_refresh_wait) > 0 && $device_refresh_wait > 0) {
push(@status_msg, "Sleeping $device_refresh_wait seconds to allow the devices to stabilize");
sleep($device_refresh_wait);
}
}
## If we're unmounting, run any unmount tasks before we do
if ($do_unmount == 1) {
for my $task (@unmount_tasks) {
run_system_cmd("$task > /dev/null 2>&1");
push(@status_msg, "Ran unmount task $task before processing the unmounts");
}
}
## Alright, lets start processing our mounts
for my $mount (@mounts) {
my $check = 0;
my $luks = $cfg->param("$mount.luks");
my $fs_type = $cfg->param("$mount.type");
my $mount_point = $cfg->param("$mount.mount");
my $mount_options = $cfg->param("$mount.mount_options");
my $mount_check = $cfg->param("$mount.check");
my $config_device = $cfg->param("$mount.device");
my $mount_map = $cfg->param("$mount.map");
my $nagios_check = $cfg->param("$mount.nagios_check");
my $mount_device = 'x';
my $physical_dev = 'x';
my @clean = $cfg->param("$mount.clean");
push(@status_msg, "\nProcessing mount $mount (state: $state, type: $fs_type, mount_point: $mount_point)");
if ($do_mount == 1) {
### Mount the devices
## First a check to make sure its not already mounted
if(-d $mount_check) {
push(@status_msg, "$mount_point is already mounted, nothing to do");
} else {
push(@status_msg, "$mount_point is not mounted, attempting to mount it as a $fs_type filesystem");
if($luks eq 'yes') {
push(@status_msg, "$mount_point is a LUKS device, attempting to determine its device name and open it");
## Here we determine our device ID based on the first, unencrypted partition with our configured label
my $devcheck = `ls -l $config_device`;
if(length($devcheck)) {
($devcheck) = $devcheck =~ /\/([a-z]*)[0-9]$/;
if(-b "/dev/$devcheck") {
$physical_dev = "/dev/$devcheck"."2";
}
}
## Check if our physical device is valid, and open it if so
if(-b $physical_dev) {
push(@status_msg, "Determined $mount_point to be physical device $physical_dev");
run_system_cmd("/sbin/cryptsetup luksOpen --key-file $key_file $physical_dev $mount_map > /dev/null 2>&1");
$mount_device = "/dev/mapper/$mount_map";
}
} else {
## Since this is not a luks device, we just take the configured device as is
$mount_device = $config_device
}
## $mount_device is now either a standard device or an open LUKS device, vadlidate it before mounting
if(-b $mount_device) {
## Device is valid, now mount it based on the fs_type
if($fs_type eq 'ext4') {
$check = run_system_cmd("mount $mount_options $mount_device $mount_point >> $error_log 2>&1");
} elsif ($fs_type eq 'zfs') {
$check = run_system_cmd("zpool import $mount_map >> $error_log 2>&1");
}
## If our mount returned good continue with setup/cleanup
if($check == 0) {
push (@status_msg, "$mount_point is successfully mounted as a $fs_type filesystem");
if($clean == 1 && @clean) {
foreach my $cleandir (@clean) {
push(@status_msg, "Deleting contents of directory: $cleandir");
run_system_cmd("mkdir -p $cleandir >> $error_log 2>&1");
run_system_cmd("chown $set_user:$set_group $cleandir >> $error_log 2>&1");
run_system_cmd("rm -rf $cleandir/* >> $error_log 2>&1");
}
}
if(length($nagios_check)) {
push(@status_msg, "Creating nagios check file: $nagios_check");
run_system_cmd("touch $nagios_check >> $error_log 2>&1");
run_system_cmd("chown $set_user:$set_group $nagios_check >> $error_log 2>&1");
}
} else {
$status = 1;
push (@status_msg, "Failed mounting $mount_point");
}
} else {
$status = 1;
push (@status_msg, "Invalid block device $mount_device, will not attempt to mount it");
}
}
} elsif($do_unmount == 1) {
### Unmount the devices
## First a check to make sure its already mounted before trying to unmount
if(-d $mount_check) {
push(@status_msg, "$mount_point is currently mounted, attempting to unmount it");
if($fs_type eq 'ext4') {
$check = run_system_cmd("umount $mount_point >> $error_log 2>&1");
} elsif ($fs_type eq 'zfs') {
$check = run_system_cmd("zpool export $mount_map >> $error_log 2>&1");
}
## If our unmount returned good continue with rest of cleanup as needed
if($check == 0) {
push (@status_msg, "$mount_point successfully unmounted");
if($luks eq 'yes') {
push (@status_msg, "Mount $mount is a LUKS device, close the mapping");
$mount_device = "/dev/mapper/$mount_map";
run_system_cmd("/sbin/cryptsetup luksClose $mount_device >> $error_log 2>&1");
if(-b $mount_device) {
push (@status_msg, "Failed closing LUKS device $mount_device");
$status = 1;
} else {
push (@status_msg, "LUKS device $mount_device successfully closed");
}
}
} else {
push (@status_msg, "Failed unmounting $mount_point");
$status = 1;
}
} else {
push(@status_msg, "$mount_point is not mounted, nothing to do");
}
}
}
## If we're mounting, run any mount tasks after we do
if ($do_mount == 1) {
for my $task (@mount_tasks) {
run_system_cmd("$task > /dev/null 2>&1");
push(@status_msg, "Ran mount task $task after processing the unmounts");
}
}
## When unmounting, based on status make a final note about drive removal
if($do_unmount == 1) {
if($status == 0) {
push (@status_msg, "\nIt is safe to remove the drives");
} else {
push (@status_msg, "\nERROR: A problem was encountered. The drives can still be removed but should be investigated.");
}
}
## Send a status email and an error email if warranted (and configured)
my $email_to = $cfg->param("mail.status_email");
my $error_email = $cfg->param("mail.error_email");
my $email_subject = $cfg->param("mail.email_subject");
my $email_data = '';
if(-e $error_log) {
my $log_data = `cat $error_log`;
if(length($log_data) > 0) {
$email_data = "The following is the contents of the error log:\n\n" . $log_data;
$status = 1;
}
}
$email_data = $email_data . join("\n", @status_msg);
if($status != 0) {
if(length($error_email) > 0) {
$email_to = "$email_to, $error_email";
}
$email_subject = "$email_subject - $state (ERROR)";
} else {
$email_subject = "$email_subject - $state (OK)";
}
if(length($email_to) > 0) {
my $msg = MIME::Lite->new( From => $cfg->param("mail.from_address"),
To => $email_to,
Subject => $email_subject,
Data => $email_data);
$msg->send;
}
exit 0;
## Function to run a system command and return the status (return code)
sub run_system_cmd {
my $cmd = $_[0];
my $return = 0;
system($cmd);
if ($? == -1) {
$return = -1
} elsif ($? & 127) {
$return = -1
} else {
$return = $? >> 8;
}
return $return;
}