#!/usr/pkg/bin/perl
use strict;

# should be running as root
if($> != 0)
{
	printf "\nWARNING: this should be run as root\n\n"
}

sub error_print_usage
{
	print <<__E;

Setup bbackupd config utility.

Bad command line parameters.
Usage:
    bbackupd-config config-dir backup-mode account-num server-hostname
        working-dir [backup directories]

Parameters:
    config-dir          is usually /etc/boxbackup
    backup-mode         is lazy or snapshot:
        lazy mode       runs continously, uploading files over a specified age
        snapshot mode   uploads a snapshot of the filesystem when instructed
                        explicitly, using bbackupctl sync
    account-num (hexdecimal) and server-hostname
                        are supplied by the server administrator
    working-dir         is usually /var/bbackupd
    backup directories  is list of directories to back up

__E
	print "=========\nERROR:\n",$_[0],"\n\n" if $_[0] ne '';
	exit(1);
}

# check and get command line parameters
if($#ARGV < 4)
{
	error_print_usage();
}

# check for OPENSSL_CONF environment var being set
if(exists $ENV{'OPENSSL_CONF'})
{
	print <<__E;

---------------------------------------

WARNING:
    You have the OPENSSL_CONF environment variable set.
    Use of non-standard openssl configs may cause problems.

---------------------------------------

__E
}

# default locations
my $default_config_location = '/etc/boxbackup/bbackupd.conf';

# command line parameters
my ($config_dir,$backup_mode,$account_num,$server,$working_dir,@tobackup) = @ARGV;

# check backup mode is valid
if($backup_mode ne 'lazy' && $backup_mode ne 'snapshot')
{
	error_print_usage("ERROR: backup mode must be 'lazy' or 'snapshot'");
}

# check server exists
{
	my @r = gethostbyname($server);
	if($#r < 0)
	{
		error_print_usage("Backup server specified as '$server', but it could not found.\n(A test DNS lookup failed -- check arguments)");
	}
}

if($working_dir !~ m~\A/~)
{
	error_print_usage("Working directory $working_dir is not specified as an absolute path");
}

# ssl stuff
my $private_key = "$config_dir/bbackupd/$account_num-key.pem";
my $certificate_request = "$config_dir/bbackupd/$account_num-csr.pem";
my $certificate = "$config_dir/bbackupd/$account_num-cert.pem";
my $ca_root_cert = "$config_dir/bbackupd/serverCA.pem";

# encryption keys
my $enc_key_file = "$config_dir/bbackupd/$account_num-FileEncKeys.raw";

# other files
my $config_file = "$config_dir/bbackupd.conf";
my $notify_script = "$config_dir/bbackupd/NotifySysadmin.sh";

# check that the directories are allowable
for(@tobackup)
{
	if($_ eq '/')
	{
		die "It is not recommended that you backup the root directory of your disc";
	}
	if($_ !~ m/\A\//)
	{
		die "Directory $_ is not specified as an absolute path";
	}
	if(!-d $_)
	{
		die "$_ is not a directory";
	}
}

# summarise configuration

print <<__E;

Setup bbackupd config utility.

Configuration:
   Writing configuration file: $config_file
   Account: $account_num
   Server hostname: $server
   Directories to back up:
__E
print '      ',$_,"\n" for(@tobackup);
print <<__E;

Note: If other file systems are mounted inside these directories, then
they will NOT be backed up. You will have to create separate locations for
any mounted filesystems inside your backup locations.

__E

# create directories
if(!-d $config_dir)
{
	printf "Creating $config_dir...\n";
	mkdir $config_dir,0755 or die "Can't create $config_dir";
}

if(!-d "$config_dir/bbackupd")
{
	printf "Creating $config_dir/bbackupd\n";
	mkdir "$config_dir/bbackupd",0700 or die "Can't create $config_dir/bbackupd";
}

if(!-d "$working_dir")
{
	printf "Creating $working_dir\n";
	if(!mkdir($working_dir,0700))
	{
		die "Couldn't create $working_dir -- create this manually and try again\n";
	}
}

# generate the private key for the server
if(!-f $private_key)
{
	print "Generating private key...\n";
	if(system("openssl genrsa -out $private_key 2048") != 0)
	{
		die "Couldn't generate private key."
	}
}

# generate a certificate request
if(!-f $certificate_request)
{
	die "Couldn't run openssl for CSR generation" unless
		open(CSR,"|openssl req -new -key $private_key -sha1 -out $certificate_request");
	print CSR <<__E;
.
.
.
.
.
BACKUP-$account_num
.
.
.

__E
	close CSR;
	print "\n\n";
	die "Certificate request wasn't created.\n" unless -f $certificate_request
}

# generate the key material for the file
if(!-f $enc_key_file)
{
	print "Generating keys for file backup\n";
	if(system("openssl rand -out $enc_key_file 1024") != 0)
	{
		die "Couldn't generate file backup keys."
	}
}

# write the notify when store full script
print "Writing notify script $notify_script\n";
open NOTIFY,">$notify_script" or die "Can't open for writing";

my $hostname = `hostname`; chomp $hostname;
my $current_username = `whoami`; chomp $current_username;
my $sendmail = `whereis sendmail`; chomp $sendmail;
$sendmail =~ s/\n.\Z//s;
# for Linux style whereis
$sendmail = $1 if $sendmail =~ /^sendmail:\s+([\S]+)/;
# last ditch guess
$sendmail = 'sendmail' if $sendmail !~ m/\S/;

print NOTIFY <<__EOS;
#!/bin/sh

# This script is run whenever bbackupd changes state or encounters a
# problem which requires the system administrator to assist:
#
# 1) The store is full, and no more data can be uploaded.
# 2) Some files or directories were not readable.
# 3) A backup run starts or finishes.
#
# The default script emails the system administrator, except for backups
# starting and stopping, where it does nothing.

SUBJECT="BACKUP PROBLEM on host $hostname"
SENDTO="$current_username"

if [ "\$1" = "" ]; then
	echo "Usage: \$0 <store-full|read-error|backup-error|backup-start|backup-finish>" >&2
	exit 2
elif [ "\$1" = store-full ]; then
	$sendmail \$SENDTO <<EOM
Subject: \$SUBJECT (store full)
To: \$SENDTO


The store account for $hostname is full.

=============================
FILES ARE NOT BEING BACKED UP
=============================

Please adjust the limits on account $account_num on server $server.

EOM
elif [ "\$1" = read-error ]; then
$sendmail \$SENDTO <<EOM
Subject: \$SUBJECT (read errors)
To: \$SENDTO


Errors occured reading some files or directories for backup on $hostname.

===================================
THESE FILES ARE NOT BEING BACKED UP
===================================

Check the logs on $hostname for the files and directories which caused
these errors, and take appropriate action.

Other files are being backed up.

EOM
elif [ "\$1" = backup-start -o "\$1" = backup-finish ]; then
	# do nothing by default
	true
else
$sendmail \$SENDTO <<EOM
Subject: \$SUBJECT (unknown)
To: \$SENDTO


The backup daemon on $hostname reported an unknown error (\$1).

==========================
FILES MAY NOT BE BACKED UP
==========================

Please check the logs on $hostname.

EOM
fi
__EOS

close NOTIFY;
chmod 0700,$notify_script or die "Can't chmod $notify_script";


# write the configuration file
print "Writing configuration file $config_file\n";
open CONFIG,">$config_file" or die "Can't open config file for writing";
print CONFIG <<__E;

StoreHostname = $server
AccountNumber = 0x$account_num
KeysFile = $enc_key_file

CertificateFile = $certificate
PrivateKeyFile = $private_key
TrustedCAsFile = $ca_root_cert

DataDirectory = $working_dir


# This script is run whenever bbackupd changes state or encounters a
# problem which requires the system administrator to assist:
#
# 1) The store is full, and no more data can be uploaded.
# 2) Some files or directories were not readable.
# 3) A backup run starts or finishes.
#
# The default script emails the system administrator, except for backups
# starting and stopping, where it does nothing.

NotifyScript = $notify_script

__E

if($backup_mode eq 'lazy')
{
	# lazy mode configuration
	print CONFIG <<__E;

# The number of seconds between backup runs under normal conditions. To avoid 
# cycles of load on the server, this time is randomly adjusted by a small 
# percentage as the daemon runs.

UpdateStoreInterval = 3600


# The minimum age of a file, in seconds, that will be uploaded. Avoids 
# repeated uploads of a file which is constantly being modified.

MinimumFileAge = 21600


# If a file is modified repeated, it won't be uploaded immediately in case 
# it's modified again, due to the MinimumFileAge specified above. However, it 
# should be uploaded eventually even if it is being modified repeatedly. This 
# is how long we should wait, in seconds, after first noticing a change. 
# (86400 seconds = 1 day)

MaxUploadWait = 86400

# If the connection is idle for some time (e.g. over 10 minutes or 600
# seconds, not sure exactly how long) then the server will give up and
# disconnect the client, resulting in Connection Protocol_Timeout errors
# on the server and TLSReadFailed or TLSWriteFailed errors on the client.
# Also, some firewalls and NAT gateways will kill idle connections after
# similar lengths of time. 
#
# This can happen for example when most files are backed up already and
# don't need to be sent to the store again, while scanning a large
# directory, or while calculating diffs of a large file. To avoid this,
# KeepAliveTime specifies that special keep-alive messages should be sent
# when the connection is otherwise idle for a certain length of time,
# specified here in seconds.
#
# The default is that these messages are never sent, equivalent to setting
# this option to zero, but we recommend that all users enable this.

KeepAliveTime = 120

__E
}
else
{
	# snapshot configuration
	print CONFIG <<__E;

# This configuration file is written for snapshot mode.
# You will need to run bbackupctl to instruct the daemon to upload files.

AutomaticBackup = no
UpdateStoreInterval = 0
MinimumFileAge = 0
MaxUploadWait = 0

__E
}

print CONFIG <<__E;

# Files above this size (in bytes) are tracked, and if they are renamed they will simply be
# renamed on the server, rather than being uploaded again. (64k - 1)

FileTrackingSizeThreshold = 65535


# The daemon does "changes only" uploads for files above this size (in bytes).
# Files less than it are uploaded whole without this extra processing.

DiffingUploadSizeThreshold = 8192


# The limit on how much time is spent diffing files, in seconds. Most files 
# shouldn't take very long, but if you have really big files you can use this 
# to limit the time spent diffing them.
#
# * Reduce if you are having problems with processor usage.
#
# * Increase if you have large files, and think the upload of changes is too 
#   large and you want bbackupd to spend more time searching for unchanged
#   blocks.

MaximumDiffingTime = 120


# Uncomment this line to see exactly what the daemon is going when it's connected to the server.

# ExtendedLogging = yes


# This specifies a program or script script which is run just before each 
# sync, and ideally the full path to the interpreter. It will be run as the 
# same user bbackupd is running as, usually root.
#
# The script must output (print) either "now" or a number to STDOUT (and a 
# terminating newline, no quotes).
#
# If the result was "now", then the sync will happen. If it's a number, then 
# no backup will happen for that number of seconds (bbackupd will pause) and 
# then the script will be run again.
#
# Use this to temporarily stop bbackupd from syncronising or connecting to the 
# store. For example, you could use this on a laptop to only backup when on a 
# specific network, or when it has a working Internet connection.

# SyncAllowScript = /path/to/intepreter/or/exe script-name parameters etc


# Where the command socket is created in the filesystem.

CommandSocket = $working_dir/bbackupd.sock

# Uncomment the StoreObjectInfoFile to enable the experimental archiving
# of the daemon's state (including client store marker and configuration)
# between backup runs. This saves time and increases efficiency when
# bbackupd is frequently stopped and started, since it removes the need
# to rescan all directories on the remote server. However, it is new and
# not yet heavily tested, so use with caution.

# StoreObjectInfoFile = $working_dir/bbackupd.state

Server
{
	PidFile = $working_dir/bbackupd.pid
}


# BackupLocations specifies which locations on disc should be backed up. Each
# directory is in the format
# 
# 	name
# 	{
# 		Path = /path/of/directory
# 		(optional exclude directives)
# 	}
# 
# 'name' is derived from the Path by the config script, but should merely be
# unique.
# 
# The exclude directives are of the form
# 
# 	[Exclude|AlwaysInclude][File|Dir][|sRegex] = regex or full pathname
# 
# (The regex suffix is shown as 'sRegex' to make File or Dir plural)
#
# For example:
# 
# 	ExcludeDir = /home/guest-user
# 	ExcludeFilesRegex = \.(mp3|MP3)\$
# 	AlwaysIncludeFile = /home/username/veryimportant.mp3
# 
# This excludes the directory /home/guest-user from the backup along with all mp3
# files, except one MP3 file in particular.
# 
# In general, Exclude excludes a file or directory, unless the directory is
# explicitly mentioned in a AlwaysInclude directive. However, Box Backup
# does NOT scan inside excluded directories and will never back up an
# AlwaysIncluded file or directory inside an excluded directory or any
# subdirectory thereof.
#
# To back up a directory inside an excluded directory, use a configuration
# like this, to ensure that each directory in the path to the important
# files is included, but none of their contents will be backed up except
# the directories further down that path to the important one.
#
# ExcludeDirsRegex = ^/home/user/bigfiles/
# ExcludeFilesRegex = ^/home/user/bigfiles/
# AlwaysIncludeDir = /home/user/bigfiles/path
# AlwaysIncludeDir = /home/user/bigfiles/path/to
# AlwaysIncludeDir = /home/user/bigfiles/path/important
# AlwaysIncludeDir = /home/user/bigfiles/path/important/files
# AlwaysIncludeDirsRegex = ^/home/user/bigfiles/path/important/files/
# AlwaysIncludeFilesRegex = ^/home/user/bigfiles/path/important/files/
# 
# If a directive ends in Regex, then it is a regular expression rather than a 
# explicit full pathname. See
# 
# 	man 7 re_format
# 
# for the regex syntax on your platform.

BackupLocations
{
__E

# write the dirs to backup
for my $d (@tobackup)
{
	$d =~ m/\A.(.+)\Z/;
	my $n = $1;
	$n =~ tr`/`-`;
	
	my $excludekeys = '';
	if(substr($enc_key_file, 0, length($d)+1) eq $d.'/')
	{
		$excludekeys = "\t\tExcludeFile = $enc_key_file\n";
		print <<__E;

NOTE: Keys file has been explicitly excluded from the backup.

__E
	}
	
	print CONFIG <<__E
	$n
	{
		Path = $d
$excludekeys	}
__E
}

print CONFIG "}\n\n";
close CONFIG;

# explain to the user what they need to do next
my $daemon_args = ($config_file eq $default_config_location)?'':" $config_file";
my $ctl_daemon_args = ($config_file eq $default_config_location)?'':" -c $config_file";

print <<__E;

===================================================================

bbackupd basic configuration complete.

What you need to do now...

1) Make a backup of $enc_key_file
   This should be a secure offsite backup.
   Without it, you cannot restore backups. Everything else can
   be replaced. But this cannot.
   KEEP IT IN A SAFE PLACE, OTHERWISE YOUR BACKUPS ARE USELESS.

2) Send $certificate_request
   to the administrator of the backup server, and ask for it to
   be signed.

3) The administrator will send you two files. Install them as
      $certificate
      $ca_root_cert
   after checking their authenticity.

4) You may wish to read the configuration file
      $config_file
   and adjust as appropriate.
   
   There are some notes in it on excluding files you do not
   wish to be backed up.

5) Review the script
      $notify_script
   and check that it will email the right person when the store
   becomes full. This is important -- when the store is full, no
   more files will be backed up. You want to know about this.

6) Start the backup daemon with the command
      /usr/pkg/sbin/bbackupd$daemon_args
   in /etc/rc.local, or your local equivalent.
   Note that bbackupd must run as root.
__E
if($backup_mode eq 'snapshot')
{
	print <<__E;

7) Set up a cron job to run whenever you want a snapshot of the
   file system to be taken. Run the command
      /usr/pkg/bin/bbackupctl -q$ctl_daemon_args sync
__E
}
print <<__E;

===================================================================

Remember to make a secure, offsite backup of your backup keys,
as described in step 1 above. If you do not, you have no backups.

__E

