#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # This txt file contains the ohbackup.pl script and the sidecar files #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # ohbackup.pl #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# #!/usr/bin/perl -w #################################################################################################################################### # # Purpose: # # This script calls the openHAB backup command. Temporary and target directory can be determined by certain options. # # The following options are availablea: # # < -o | --outdir > specifies the output directory where all zip files are put # < -t | --tmpdir > specifies a temporary directory. A directory on tmpfs is a good candidate # < -m | --mount > specifies a mount point in fstab which is mounted before and unmounted after the backups # < -f | --full > adds the full option to the openHAB backup # < -h | --help > prints the usage info to the terminal # # In addition sidecar files (in the same directory as this script) trigger further backups: # # ./ohbackup_crontabs = a list of crontab files which are added to a zip file. # ./ohbackup_deconz = credentials to perform a backup of deCONZ (i. e. Raspberry). # ./ohbackup_files = a list of files which are added to a zip file. # ./ohbackup_homedirs = a list of local accounts who's home directories are zipped # # History: # # 20220113 - Reinhold Wagner - Begin of Development # # License and warranty: # # This software is free open-source software distributed under the terms and conditions of the GNU General Public License (GPL) # version 2 or (at your option) any later version. For using this script, no restrictions apply. You can further redistribute # and/or modify this software under the terms of the GPL. # # 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. # # For details, have a look at the full text of the GPL: https://opensource.org/licenses/GPL-2.0 #################################################################################################################################### #################################################################################################################################### # Main logic + housekeeping #################################################################################################################################### use Getopt::Long qw(:config ignore_case_always bundling); use File::Path qw(make_path remove_tree); use Tie::File; use Fcntl qw(O_RDONLY); # tie r/o option $rc = 0; # return code of the script if (housekeeping()) { # backup files (sidecar file ./ohbackup_files) if (-r $bkpctrl{files}->[0]) { # if sidecar file is readable ... msg("Creating archive $bkpctrl{files}->[2]"); # print info message to terminal tie(my @input, 'Tie::File', $bkpctrl{files}->[0], mode => O_RDONLY) or die "Could not open file $bkpctrl{files}->[0]!\n"; foreach my $ifile (@input) { # walk through the input file next if $ifile =~ m!^\s*(#|//|$)!; # ignore comment lines if (-r $ifile) { # if the file is readable cpfile($ifile, $bkpctrl{files}->[1]); # copy it to a temporary directory } else { # if not msg(qq(Could not read file "$ifile"), 2); # print error message } } untie @input; system "cd $bkpctrl{files}->[1]; /usr/bin/zip -qr $bkpctrl{files}->[2] ."; # create the zip archive } # backup crontabs (sidecar file ./ohbackup_crontabs) if (-r $bkpctrl{crontabs}->[0]) { # if sidecar file is readable ... msg("Creating archive $bkpctrl{crontabs}->[2]"); # print info message to terminal my $ctdir = "/var/spool/cron/crontabs/"; # crotabs directory tie(my @input, 'Tie::File', $bkpctrl{crontabs}->[0], mode => O_RDONLY) or die "Could not open file $bkpctrl{crontabs}->[0]!\n"; foreach my $ctname (@input) { # walk through the input file next if $ctname =~ m!^\s*(#|//|$)!; # ignore comment lines my $ifile = $ctdir . $ctname; # create full path of crontab if (-r $ifile) { # if the crontab is readable system "/usr/bin/cp -p $ifile $bkpctrl{crontabs}->[1]/"; # copy it to a temporary directory } else { # if not msg(qq(Could not read crontab "$ctname"), 2); # print error message } } untie @input; system "cd $bkpctrl{crontabs}->[1]; /usr/bin/zip -qr $bkpctrl{crontabs}->[2] ."; # create the zip archive } # backup home directories (sidecar file ./ohbackup_homedirs) if (-r $bkpctrl{homedirs}->[0]) { # if sidecar file is readable ... tie(my @input, 'Tie::File', $bkpctrl{homedirs}->[0], mode => O_RDONLY) or die "Could not open file $bkpctrl{homedirs}->[0]!\n"; foreach (@input) { # walk through the input file next if m!^\s*(#|//|$)!; # ignore comment lines my $user = $_; chomp(my $hdir = qx(/usr/bin/grep "^$user" /etc/passwd | /usr/bin/cut -d ":" -f 6)); if (defined $hdir and $hdir ne "") { # if the home directory from /etc/passwd exists my $outfn = $bkpctrl{homedirs}->[2]; # get output file name (template) $outfn =~ s/#USER#/$user/; # replace place holder by user name msg("Creating archive $outfn"); # print info message to terminal system "cd $hdir; /usr/bin/zip -qr $outfn ."; # create zip archive } else { msg("Could not determine home directory of user $user!", 2); } } untie @input; } # backup deCONZ (sidecar file ./ohbackup_deconz) if (-r $bkpctrl{deconz}->[0]) { # if sidecar file is readable ... msg("Creating deCONZ backup"); # print info message to terminal my $URL = my $APIKEY = my $PATH = my $MOUNT = ""; # initialize variable for parameters tie(my @input, 'Tie::File', $bkpctrl{deconz}->[0], mode => O_RDONLY) or die "Could not open file $bkpctrl{deconz}->[0]!\n"; foreach (@input) { # walk through the input file next if m!^\s*(#|//|$)!; # ignore comment lines if (/^\s*(\w+)\s*=\s*(.+)\s*$/) { # is it a Key=Value pair? my $parm = uc $1; # parameter (key) my $valu = $2; # value case: { # put the values to the parameter variables ($parm eq "URL") and $URL = $valu, last case; ($parm eq "APIKEY") and $APIKEY = $valu, last case; ($parm eq "PATH") and $PATH = $valu, last case; ($parm eq "MOUNT") and $MOUNT = $valu, last case; msg(qq(Wrong parameter "$parm" in $bkpctrl{deconz}->[0] - ignored!), 2); } } else { msg("Wrong or malformed parameter in $bkpctrl{deconz}->[0] - ignored!", 2); } } untie @input; if ($URL ne "" and $APIKEY ne "" and $PATH ne "") { # if we have all needed parameters ... if ($MOUNT eq "" or mount($MOUNT)) { # = mount command to be executed? my $curlcmd = qq(/usr/bin/curl -s -k -H "Content-Type: application/json" -X "POST /api/${APIKEY}/config/export" $URL); chomp(my $curlresp = qx($curlcmd)); # get backup file from deCONZ if ($curlresp =~ /\"success\"/) { if (-e $PATH) { system "/usr/bin/cp -p $PATH $bkpctrl{deconz}->[2]"; } else { msg("Could not find the deCONZ backup, PATH seems to be wrong!", 2); } } else { msg(qq(Calling deCONZ URL was not successfull! Response was "$curlresp"), 2); } } } else { msg("$bkpctrl{deconz}->[0] must contain URL, APIKEY and PATH!", 2); } } # run openHAB backup tool msg("Calling openHAB backup tool"); system "$bkpcmd $ohbktool" . (($opt_full) ? " --full" : ""); # unmount the mounted shares system "/usr/bin/umount -f $_ >/dev/null 2>&1" foreach (@umounts); } else { msg(qq(Aborted due to the above mentioned issue. For help execute "$0 --help"), 2); $rc = 1; } # cleanup foreach (keys %bkpctrl) { my $dir = $bkpctrl{$_}->[1]; remove_tree($dir) if ($dir ne "" and -e $dir); } $ENV{OPENHAB_BACKUPS} = $OHBKP if defined $OHBKP; $ENV{OPENHAB_BACKUPS_TEMP} = $OHBKT if defined $OHBKT; exit $rc; #----------------------------------------------------------------------------------------------------------------------------------- # housekeeping #----------------------------------------------------------------------------------------------------------------------------------- # Prepare the environment #----------------------------------------------------------------------------------------------------------------------------------- sub housekeeping { my $hkrc = 1; # global variables @umounts = (); # Mount points which must be unmounted at the end if ($> != 0) { msg("This script must be run as root!", 2); $hkrc = 0; } if ($hkrc) { # check the options $opt_odir = ""; # output directory $opt_tdir = "/tmp"; # temprary directory $opt_mnt = ""; # mount point $opt_full = 0; $opt_help = 0; GetOptions( 'o|outdir=s' => \$opt_odir , 't|tmpdir=s' => \$opt_tdir , 'm|mount=s' => \$opt_mnt , 'f|full' => \$opt_full , 'h|help|?' => \$opt_help , ) or $hkrc = 0; } $hkrc = 0 if ($opt_help); # end housekeeping if help was requested # make sure the openHAB evironment variable OPENHAB_RUNTIME is defined if ($hkrc and ! defined $ENV{OPENHAB_RUNTIME}) { msg("Make sure the openHAB environment variable OPENHAB_RUNTIME is set!", 2); $hkrc = 0; } # check mount point if --mount was specified if ($hkrc and $opt_mnt ne "") { $hkrc = 0 unless mount($opt_mnt); } # check temporary and target directories if ($hkrc) { $opt_tdir .= "/" unless ($opt_tdir =~ m!/$!); unless (-w $opt_tdir) { msg("Cannot write to directory $opt_tdir", 2); $hkrc = 0; } } if ($hkrc and $opt_odir ne "") { unless (-w $opt_odir) { msg("Cannot write to directory $opt_odir", 2); $hkrc = 0; } } # determine backup command and directories if ($hkrc) { $bkpcmd = $ENV{OPENHAB_RUNTIME} . '/bin/backup'; # openHAB backup command unless (-x $bkpcmd) { msg("Cannot execute backup command $bkpcmd. Please check environment variable OPENHAB_RUNTIME", 2); $hkrc = 0; } else { $OHBKP = $ENV{OPENHAB_BACKUPS} || ""; # to be reset before exit $OHBKT = $ENV{OPENHAB_BACKUPS_TEMP} || ""; # to be reset before exit $ENV{OPENHAB_BACKUPS_TEMP} = $opt_tdir . "ohbkptmp"; # openHAB backup temp directory my @ohbkpdirs = ($ENV{OPENHAB_BACKUPS_TEMP}); if ($opt_odir ne "") { $ENV{OPENHAB_BACKUPS} = $opt_odir; # openHAB backup target directory } else { $ENV{OPENHAB_BACKUPS} = $opt_tdir . "ohbackup"; # openHAB backup target directory push(@ohbkpdirs, $ENV{OPENHAB_BACKUPS}); } foreach my $dir (@ohbkpdirs) { remove_tree($dir) if (-e $dir); unless (make_path($dir)) { msg("Could not create directory $dir", 2); $hkrc = 0; last; } } } } # file names if ($hkrc) { chomp(my $fnext = qx(/usr/bin/date +"%y%m%d-%H%M%S")); # date extension for output file names my $sidecardir = $1 . "/" if ($0 =~ m!^(.+)/.+$!); # determine my current directory $ohbktool = "$ENV{OPENHAB_BACKUPS}/$fnext.openhab_backup.zip"; %bkpctrl = ( "files" => [ $sidecardir . "ohbackup_files" , # [0] = input file name "$ENV{OPENHAB_BACKUPS_TEMP}/files/" , # [1] = temporary directory "$ENV{OPENHAB_BACKUPS}/$fnext.openhab_files.zip" , # [2] = gzip file name ] , "homedirs" => [ $sidecardir . "ohbackup_homedirs" , "" , "$ENV{OPENHAB_BACKUPS}/$fnext.openhab_homedir_#USER#.zip" , ] , "crontabs" => [ $sidecardir . "ohbackup_crontabs" , "$ENV{OPENHAB_BACKUPS_TEMP}/crontabs/" , "$ENV{OPENHAB_BACKUPS}/$fnext.openhab_crontabs.zip" , ] , "deconz" => [ $sidecardir . "ohbackup_deconz" , "" , "$ENV{OPENHAB_BACKUPS}/$fnext.raspbee_deconz_config.dat" , ] , ); # create temporary directories foreach my $inp (keys %bkpctrl) { if (-r $bkpctrl{$inp}->[0] and $bkpctrl{$inp}->[1] ne "") { # is input file readable? unless (mkdir $bkpctrl{$inp}->[1]) { # yes, create temporary directory msg("Could not create directory $bkpctrl{$inp}->[1]", 2); $hkrc = 0; } } } } return $hkrc; } #################################################################################################################################### # Supporting functions #################################################################################################################################### #----------------------------------------------------------------------------------------------------------------------------------- # mount #----------------------------------------------------------------------------------------------------------------------------------- # Mount a remote file system. The mount point must be in /etc/fstab. #----------------------------------------------------------------------------------------------------------------------------------- sub mount { my $mpoint = shift || ""; my $mrc = 0; if ($mpoint !~ m!^/!) { # does the moint point begin with /? msg(qq!Mount point $mpoint must be an absolute path (beginning with "/")!, 2); } elsif (! -e $mpoint) { # does the moint point exist? msg(qq(Mount point $mpoint does not exist!), 2); } elsif (grep m!^.+ on $mpoint type!, qx(/usr/bin/mount)) { msg(qq(Mount point $mpoint is already in use!), 2); } else { system "/usr/bin/mount $mpoint"; if ($? != 0) { msg("Could not mount $mpoint!", 2); } else { push @umounts, $mpoint; $mrc = 1; } } return $mrc; } #----------------------------------------------------------------------------------------------------------------------------------- # cpfile #----------------------------------------------------------------------------------------------------------------------------------- # Rename a file path by replacing / by ^, then copy it to the target directory #----------------------------------------------------------------------------------------------------------------------------------- sub cpfile { my $in = shift || ""; my $out = shift || ""; my $outfn = $in; $outfn =~ tr!/!^!; $out .= $outfn; system "/usr/bin/cp -p $in $out"; } #----------------------------------------------------------------------------------------------------------------------------------- # msg #----------------------------------------------------------------------------------------------------------------------------------- # Print a message to STDOUT or STDERR #----------------------------------------------------------------------------------------------------------------------------------- sub msg { my $text = shift || "You forgot to provide a message text!"; my $stdx = shift || 1; my $channel = ($stdx eq 2) ? "STDERR" : "STDOUT"; my $txtpref = ($stdx eq 2) ? "ohbackup E>" : "ohbackup I>"; print $channel "$txtpref $text\n"; } #################################################################################################################################### # Usage info #################################################################################################################################### #----------------------------------------------------------------------------------------------------------------------------------- # usage #----------------------------------------------------------------------------------------------------------------------------------- sub usage { print <] [--outdir ] [--mount ] [--full] [--help] The purpose of this script is to backup the openHAB configuration by calling the openHAB backup tool. Sidecar files are used to control the backup of additional things. Parameters: ----------- --tmpdir | -t Temporary directory which will be used by this script and also by the openHAB backup tool. Default is /tmp. --outdir | -o All data (basically ZIP files) will be put there. If this parameter is omitted, then the target will be <--tmpdir>/ohbackup --mount | -m Directory to be mounted. This could be a remote share to be used as target. It must be in fstab. --full | -f Adds the "full" option to the openHAB backup tool Sidecar files: -------------- Sidecar files can be used to have $0 backup additional items. The files must be in the same directory as ohbackup.pl. These files add additional items to be backed up: ohbackup_crontabs This files contains local accounts names whos crontabs are backed up. ohbackup_homedir This file contains local account names whos home directories are backed up. ohbackup_files This contains a list of files (full path) which will be backed up. At the end there is a zipped file containing the files. The names are the complete path where / are replaced by ^. ohbackup_deconz This files contains the URL and API Key to call the Phoscon Rest API, the complete path of the tar file and a possible mountpoint if the gateway is on a remote server. EOF } #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # ohbackup.crontabs #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# // This file contains a list of usernames whos crontabs will be backed up // openhabian #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # ohbackup.deconz #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# // Credentials needed to backup the ConBee or RaspBee configuration // URL of the deCONZ gateway // URL = http://localhost:8090 // Valid API key // APIKEY = ABCDE12345 // mointpoint of /etc/fstab if a mount is needed prior to the backup // MOUNT = /xxx/yyy // Path where the backup file can be found // PATH = /path/to/deCONZ.tar.gz #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # ohbackup.files #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# // List of files (full path) which should be put into a zip file // /etc/hosts // /etc/systemd/timesyncd.conf #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # ohbackup.homedirs #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# // List of user names whos home directories should be backed up // openhabian