commit d73ec91cf25141d3ba867658b845aa7c47b11827 Author: Jakobus Schürz Date: Tue Mar 5 13:20:31 2019 +0100 initial commit diff --git a/.builddeb b/.builddeb new file mode 100644 index 0000000..e69de29 diff --git a/.publish-git b/.publish-git new file mode 100644 index 0000000..e69de29 diff --git a/.update b/.update new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67a1326 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Jakobus Schürz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* 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. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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 THE COPYRIGHT HOLDER OR 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dc0f077 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +DIR=$(shell basename $(CURDIR)) + +VERSION=`gawk '$$1 == "Version:"{print $$2}' $(DIR)/DEBIAN/control` +ARCH=`gawk '$$1 == "Architecture:"{print $$2}' $(DIR)/DEBIAN/control` +COMMIT = $(shell date "+xe%Y%m%d_%H%M%S") + +SUBDIRS := $(shell find $(DIR) -type d -print) +FILTER := $(abspath .git% %.deb .publish-git .builddeb %.swp Makefile) +FILTERORIG := $(abspath .git% %.deb .publish-git .builddeb %.swp Makefile) $(shell test -e noupdate.files && cat noupdate.files) /DEBIAN% +FILES := $(filter-out $(FILTER), $(abspath $(shell find . -mindepth 1 -type f -print) )) +ORIGS := $(filter-out $(FILTERORIG), $(realpath $(subst ./$(DIR),,$(shell sudo find . -mindepth 2 -type f -print)))) +#FILES := $(filter-out $(FILTER), $(abspath $(shell find . -mindepth 1 -type f -exec echo {} \;) )) +#ORIGS := $(filter-out $(FILTERORIG), $(realpath $(subst ./$(DIR),,$(shell sudo find . -mindepth 2 -type f -exec echo {] \;)))) +FILESGIT := $(filter-out $(abspath .git%), $(abspath $(shell find . -mindepth 1 -type f -print))) + +#all: $(DIR)/DEBIAN/control + +#$(DIR)/DEBIAN/control: $(FILES) + +-include Makefile.repo + +all: .builddeb + @#echo FILE $(FILESGIT) + +.builddeb: $(FILES) + @#echo FILT $(FILTER) + @#echo FILE $(FILES) + @echo `gawk -f ../increment.awk $(DIR)/DEBIAN/control` + sed -e "s/^Version:.*/`gawk -f ../increment.awk $(DIR)/DEBIAN/control`/" $(DIR)/DEBIAN/control > $(DIR)/DEBIAN/control.tmp + mv $(DIR)/DEBIAN/control.tmp $(DIR)/DEBIAN/control + fakeroot dpkg-deb --build $(DIR) "$(DIR)_$(VERSION)_$(ARCH).deb" + ln -sf "$(DIR)_$(VERSION)_$(ARCH).deb" "$(DIR)_current_$(ARCH).deb" + aptly repo add xundeenergie "$(DIR)_$(VERSION)_$(ARCH).deb" + touch .builddeb + +buildonlydeb: $(FILES) + @#echo FILT $(FILTER) + @#echo FILE $(FILES) + @echo `gawk -f ../increment.awk $(DIR)/DEBIAN/control` + sed -e "s/^Version:.*/`gawk -f ../increment.awk $(DIR)/DEBIAN/control`/" $(DIR)/DEBIAN/control > $(DIR)/DEBIAN/control.tmp + mv $(DIR)/DEBIAN/control.tmp $(DIR)/DEBIAN/control + fakeroot dpkg-deb --build $(DIR) "$(DIR)_$(VERSION)_$(ARCH).deb" + ln -sf "$(DIR)_$(VERSION)_$(ARCH).deb" "$(DIR)_current_$(ARCH).deb" + +.update: $(ORIGS) + @#for i in $(ORIGS); do $$i;done + @echo "$(ORIGS)" + @echo "Copy originals to $(DIR)" + @for i in $(ORIGS); do sudo cp -uv $$i $(DIR)$$i;done + touch .update + +.publish-git: $(FILESGIT) + fakeroot git add . + fakeroot git commit -m $(COMMIT) && git push origin master || exit 0 + touch .publish-git + + +pull-git: + git pull origin || exit 0 diff --git a/Makefile.old b/Makefile.old new file mode 100644 index 0000000..8f5008f --- /dev/null +++ b/Makefile.old @@ -0,0 +1,30 @@ +DIR=$(shell basename $(CURDIR)) + +VERSION=`gawk '$$1 == "Version:"{print $$2}' $(DIR)/DEBIAN/control` +ARCH=`gawk '$$1 == "Architecture:"{print $$2}' $(DIR)/DEBIAN/control` +COMMIT = $(shell date "+xe%Y%m%d_%H%M%S") + +SUBDIRS := $(shell find $(DIR) -type d -print) +FILTER := $(abspath .git% %.deb .%) +FILTERORIG := $(abspath .git% %.deb) /DEBIAN% +FILES := $(filter-out $(FILTER), $(abspath $(shell find . -mindepth 1 -type f -print))) +ORIGS := $(filter-out $(FILTERORIG), $(realpath $(subst ./$(DIR),,$(shell find . -mindepth 2 -type f -print)))) +FILESGIT := $(filter-out $(abspath .git%), $(abspath $(shell find . -mindepth 1 -type f -print))) + +all: $(DIR)/DEBIAN/control + +$(DIR)/DEBIAN/control: $(FILES) + echo DIR $(DIR) + sed -e "s/^Version:.*/`gawk -f ../increment.awk $(DIR)/DEBIAN/control`/" $(DIR)/DEBIAN/control > $(DIR)/DEBIAN/control.tmp + mv $(DIR)/DEBIAN/control.tmp $(DIR)/DEBIAN/control + sudo dpkg-deb --build $(DIR) "$(DIR)_$(VERSION)_$(ARCH).deb" + aptly repo add xundeenergie "$(DIR)_$(VERSION)_$(ARCH).deb" + +update: + for i in $(ORIGS); do sudo cp -u $$i $(DIR)$$i;done + +publish-git: $(FILESGIT) + sudo git add . + git commit -m $(COMMIT) + git push origin master + diff --git a/README.md b/README.md new file mode 100755 index 0000000..22f3aa3 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# mkbackup-btrfs +Make snapshots recursively from btrfs-subvolumes +This scripts are written in python3 and replace https://github.com/xundeenergie/mkbtrbackup + + +## Automatic installation with script + +Start a debian-live session. Download a netboot.iso or any other stretch-live.iso +Take care, that you've installed btrfs-progs and kernel in the highest possible version. +Download and install the package mkbackup-btrfs.deb (always a symlink to the latest package) from https://github.com/xundeenergie/mkbackup-btrfs +Or just download and make it executeable + wget https://raw.githubusercontent.com/xundeenergie/mkbackup-btrfs/master/mkbackup-btrfs/usr/local/bin/create-btrfs-subs.sh + chmod a+x create-btrfs-subs-sh + +Then create a new as big as possible partition and format it with btrfs. + +If you want to use UEFI, your partition-table must be GPT. For UEFI you need an extra partition for ESP (Efi System Partition). Just lool on other places how to format a Disk for the usage with (U)EFI. + +If you've created a big btrfs-partition, mount it in your live-system: + mount /dev/sdXY /mnt -osubvolume=/,compress + +X is your drive, Y is the number oft the btrfs-partition on drive X. + +then change to /mnt + cd /mnt + +and run the script + sudo /path/to/download/create-btrfs-subs.sh + +/path/to/download is where you've downloaded the script before. + +You will end up in a chroot of your new installed ground-system. It is not bootable now. +Install a kernel, tzdata, initramfs, grub or refind, install other packages you need (firmware, network-manager, desktops...) + + +## Manually creation of the subvolume-structure, no installation! +You have to prepare your installation with some subvolumes. + +First create a directory + + mkdir -p /var/cache/btrfs_pool_SYSTEM + +and + + mkdir -p /var/cache/backup + +The first is for the local HD, to mount the whole btrfs-partition, the btrfs-pool. +The second one is for the external HD to save the backup. +This two directories are hardcoded for the default-configuration in the python-skript. + +Mount your btrfs-partition to /var/cache/btrfs_pool_SYSTEM + + mount -t btrfs -ocompress=lzo /dev/sdXY /var/cache/btrfs_pool_SYSTEM + +You neet two major subvolumes. + +One for your system, which is snapshotted on every upgrade/update, on successfull boots and so on. +The other one for data you will need accurat even if you boot from an older snapshot (recover your system, you need /home, /var/spool accurat - it's user-data!). + +The first is called for example: "@debian" +The second one is hardcoded with "`__ALWAYSCURRENT__`" + +``` +btrfs subvol create /var/cache/btrfs_pool_SYSTEM/@debian +btrfs subvol create /var/cache/btrfs_pool_SYSTEM/__ALWAYSCURRENT__ +``` + +The system mounts the default-subvolume on bootup. So be sure, that @debian is your default-subvolume. +prepare your /etc/fstab to mount the always current subvolumes from `__ALWAYSCURRENT__` +Create the following subvolumes there: + home + opt + srv + usr-local + var-cache + var-lib-mpd + var-lib-named + var-log + var-opt + var-spool + var-spool-dovecot + var-tmp + var-www + +Look at the fstab-example for mounting all this subvolumes. + +Copy your data to this subvolumes + + cp --reflink=always -ar source destination + +Reboot an check if all this subvolumes are mounted correctly. You can clean the original directories in @debian while they are overmounted with the new ones, if you go to /var/cache/btrfs_pool_SYSTEM/@debian/sub/vol/ume and delete it there. + +Be carefull. If you copy the systemd-units to your system, the timers and units are enabled!! Disable all the snapshotting with: + + systemctl stop mkbackup.target + +and disable it + + systemctl disable mkbackup.target + + +Enable and start it if all the data and subvolume-structure is correct and working. + +You can make your first snapshot with: + + systemctl start mkbackup@manually.service + +##mlocate: +To avoid, that mlocate searches the backups, edit its configuration + + /etc/updatedb.conf + PRUNE_BIND_MOUNTS="yes" + # PRUNENAMES=".git .bzr .hg .svn" + PRUNEPATHS="/tmp /var/spool /media /backup /backup-local /var/cache/backup /var/cache/btrfs_pool_SYSTEM" + PRUNEFS="NFS nfs nfs4 rpc_pipefs afs binfmt_misc proc smbfs autofs iso9660 ncpfs coda devpts ftpfs devfs mfs shfs sysfs cifs lustre tmpfs usbfs udf fuse.glusterfs fuse.sshfs curlftpfs fuse.MksnapshotFS.py" + +==Ignore Subvolumes on creating a System-Snapshot +If you want to ignore a subvolume from making a backup-Snapshot, you can handle this on several ways. +The easiest way is to drop a Drop-In-File for example like this for a guest-session-home: + + editor /etc/mkbackup-btrfs.conf.d/guestsession.conf + + [DEFAULT] + ignore = +/home/gast + +The filename doesn't matter. But it must end in ".conf" +This can be done by placing such a Drop-In with a debian-package, or manually. +You can choose, if it should be ignored generally or only on specific interval-snapshots. +The example above appends /home/gast to an existing list of ignored snapshots. + + [DEFAULT] + ignore = /home/gast + +this replaces every ignore-list with only this subvolume "/home/gast" +the "+" before the subvolume means, that the subvolume(s) given are being appended to a existing list. Without a "+", the list is replaced by the given. + +If you want to ignore a specific subvolume additionally on a specific interval (f.e. /var/www should not be backed up on hourly snapshots), place this in a file: + + [hourly] + ignore = +/var/www + +You can set the ignore-Option in every section also in /etc/mkbackup-btrfs.conf +It works the same way. DEFAULT is valid to every interval, and default-Values get overwritten or extended (missing or given "+") by the interval-sections + +To ignore several subvolumes when using mkbackup-btrfs from commandline, just use the -i option (more than once) + + mkbackup -v -t manually -i /home/guest -i /var/www create SNP @debian + +This overrides settings from config-files. + + +TODO: +- Regular-Expressions for ignoring subvolumes. Test and describe it in Todo + +=Changelog + +18.9.2016: + -added experimental new code btrfssubvols.py - not working yet!! + -new library in /usr/lib/python3/dist-packages for config-parsing + -new Fuse-Filesystem for usermounting the backups and snapshots in $HOME/backup + diff --git a/files/DEBIAN/conffiles b/files/DEBIAN/conffiles new file mode 100644 index 0000000..fdc5a51 --- /dev/null +++ b/files/DEBIAN/conffiles @@ -0,0 +1,5 @@ +/etc/apt/apt.conf.d/00mksnapshot_btrfs +lib/systemd/system/timer-aptupgrade.target +lib/systemd/system/timer-aptupgrade.timer +lib/systemd/system/timer-plugin.target +lib/systemd/system/timer-plugin.timer diff --git a/files/DEBIAN/control b/files/DEBIAN/control new file mode 100644 index 0000000..70449c5 --- /dev/null +++ b/files/DEBIAN/control @@ -0,0 +1,14 @@ +Package: mkbackup-btrfs +Version: 0.8.48 +Section: backup +Priority: extra +Architecture: all +Maintainer: Jakobus Schürz +Homepage: http://github.com/xundeenergie/mksnapshot +Provides: mkbackup-btrfs +Depends: python3, python3-progressbar, python3-psutil, btrfs-progs, systemd-alt-cron, python3-paramiko, python-paramiko, python-fuse, deb-systemd-helper-new, system-notification +Recommends: python3-pip, xe-base-config +Suggests: install-on-btrfs +Description: Backup-Suite for btrfs written in python3 + This backup-suite handles all around making backups on btrfs-filesystems, including a gnome-shell-extension + This version includes also handling for remote (via ssh) locations for source and/or backup. diff --git a/files/DEBIAN/postinst b/files/DEBIAN/postinst new file mode 100755 index 0000000..cfe0062 --- /dev/null +++ b/files/DEBIAN/postinst @@ -0,0 +1,93 @@ +#!/bin/sh + + + +# postinst script for webpy-example +# +# see: dh_installdeb(1) + +#_DEB_SYSTEMD_HELPER_DEBUG=1 + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +# source debconf library +#. /usr/share/debconf/confmodule + +# For user-services only, for earch target an extra section! for system-services user deb-systemd* +# put in full unit-name. for example "mkbackup@hourly.service" + + +case "$1" in + + configure) + # normal systemd-units + #SERVICES="mkbackup.target backup.automount var-cache-backup.automount btrfs-scrub@var-cache-btrfs_pool_SYSTEM.service" + SERVICES="mkbackup.target backup.automount var-cache-backup.automount" + for s in $SERVICES; do + deb-systemd-helper-new unmask $s >/dev/null || true + if deb-systemd-helper-new --quiet was-enabled $s; then + # Enables the unit on first installation, creates new + # symlinks on upgrades if the unit file has changed. + deb-systemd-helper-new enable $s >/dev/null || true + else + # Update the statefile to add new symlinks (if any), which need to be + # cleaned up on purge. Also remove old symlinks. + deb-systemd-helper-new update-state $s >/dev/null || true + fi + done + + # instantiated systemd-units + + DEVICE=$(awk '$9 == "btrfs" && $5 == "/" {gsub("/dev/","",$10);print $10}' /proc/self/mountinfo) + ROT=$(cat /sys/block/$(printf '%s' "$DEVICE" | sed 's/[0-9]//g')/queue/rotational) + if test $ROT -eq 0; then + INTERVALS="hourly daily weekly aptupgrade plugin afterboot manually" + else + INTERVALS="aptupgrade plugin manually" + fi + CINT=$(for i in $INTERVALS;do echo "mkbackup@${i}.service" ; done) + SERVICES="btrfs-scrub@var-cache-btrfs_pool_SYSTEM.service mkbackup-conf@mkbackup\x2dbtrfs.path mkbackup-conf@mkbackup\x2dbtrfs.service $CINT" + + for s in $SERVICES; do + #deb-systemd-helper-new-xe unmask $s >/dev/null || true + deb-systemd-helper-new unmask $s || true + # was-enabled defaults to true, so new installations run enable. + echo "enable $s" + if deb-systemd-helper-new --quiet was-enabled $s; then + # Enables the unit on first installation, creates new + # symlinks on upgrades if the unit file has changed. + deb-systemd-helper-new enable $s >/dev/null || true + else + # Update the statefile to add new symlinks (if any), which need to be + # cleaned up on purge. Also remove old symlinks. + deb-systemd-helper-new update-state $s >/dev/null || true + fi + done + ;; + abort-upgrade|abort-remove|abort-deconfigure) + exit 0 + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; + +esac + + +exit 0 + + diff --git a/files/DEBIAN/postrm b/files/DEBIAN/postrm new file mode 100755 index 0000000..43ca2d1 --- /dev/null +++ b/files/DEBIAN/postrm @@ -0,0 +1,53 @@ +#! /bin/sh + +set -e + +# In case this system is running systemd, we make systemd reload the unit files +# to pick up changes. +if [ -d /run/systemd/system ] ; then + systemctl --system daemon-reload >/dev/null || true +fi + + + +# System-Services: +# put in full unit-name. for example "mkbackup@hourly.service" +#SERVICES="mkbackup.target backup.automount var-cache-backup.automount btrfs-scrub@var-cache-btrfs_pool_SYSTEM.service mkbackup@manually.service mkbackup@aptupgrade.service mkbackup@daily.service mkbackup@weekly.service mkbackup@monthly.service mkbackup@plugin.service mkbackup@manually.service" +SERVICES="mkbackup.target backup.automount var-cache-btrfs_pool_SYSTEM.automount" +INTERVALS="hourly daily weekly aptupgrade plugin afterboot manually" +CINT="$(for i in $INTERVALS;do echo "mkbackup@${i}.service" ; done)" +INSTSERVICES="btrfs-scrub@var-cache-btrfs_pool_SYSTEM.service mkbackup-conf@mkbackup\x2dbtrfs.path mkbackup-conf@mkbackup\x2dbtrfs.service" + + +case "$1" in + purge) +# systemctl disable $SERVICES $INSTSERVICES +# for s in $INSTSERVICES;do +# rm -rf /lib/systemd/system/${s} +# done + + if [ -x "/usr/bin/deb-systemd-helper-new" ]; then + deb-systemd-helper-new purge $SERVICES $INSTSERVICES $CINT >/dev/null + deb-systemd-helper-new unmask $SERVICES $INSTSERVICES $CINT >/dev/null + fi + ;; + abort-upgrade) + ;; + remove) +# systemctl mask $SERVICES +# for s in $INSTSERVICES;do +# ln -s /dev/null /lib/systemd/system/${s} +# done + if [ -x "/usr/bin/deb-systemd-helper-new" ]; then + deb-systemd-helper-new mask $SERVICES $INSTSERVICES $CINT >/dev/null + fi + ;; + upgrade|failed-upgrade|abort-install|disappear) + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 0 + ;; + +esac diff --git a/files/etc/apt/apt.conf.d/00mksnapshot_btrfs b/files/etc/apt/apt.conf.d/00mksnapshot_btrfs new file mode 100755 index 0000000..3c4b9dd --- /dev/null +++ b/files/etc/apt/apt.conf.d/00mksnapshot_btrfs @@ -0,0 +1,2 @@ +//Take a snapshot befor upgrading the system +DPkg::Pre-Invoke {"/bin/systemctl start timer-aptupgrade.timer";}; diff --git a/files/etc/mkbackup-btrfs.conf.d/docker.conf b/files/etc/mkbackup-btrfs.conf.d/docker.conf new file mode 100644 index 0000000..73d7fb0 --- /dev/null +++ b/files/etc/mkbackup-btrfs.conf.d/docker.conf @@ -0,0 +1,2 @@ +[DEFAULT] +ignore=+var-lib-docker diff --git a/files/etc/mkbackup-btrfs.conf.d/l10n-de.conf b/files/etc/mkbackup-btrfs.conf.d/l10n-de.conf new file mode 100644 index 0000000..154895b --- /dev/null +++ b/files/etc/mkbackup-btrfs.conf.d/l10n-de.conf @@ -0,0 +1,34 @@ +[DEFAULT] +Description = Erstellt ein Backup + +[dmin] +Description = + alle 10 Minuten + +[hourly] +Description = + jede Stunde + +[daily] +Description = + jeden Tag + +[weekly] +Description = + jede Woche + +[monthly] +Description = + jedes Monat + +[yearly] +Description = + einmal jedes Jahr + +[afterboot] +Description = + nach jedem erfolgreichen Boot + +[plugin] +Description = + nach jedem Einstecken des externen Backup-Mediums + +[manually] +Description = + bei manuellem Aufruf + +[aptupgrade] +Description = + vor jedem Systemupgrade oder jeder Paketinstallation + + diff --git a/files/etc/mkbackup-btrfs.conf.d/live-builds.conf b/files/etc/mkbackup-btrfs.conf.d/live-builds.conf new file mode 100644 index 0000000..24a7804 --- /dev/null +++ b/files/etc/mkbackup-btrfs.conf.d/live-builds.conf @@ -0,0 +1,11 @@ +[daily] +ignore=+/home/jakob/src/live-images + +[weekly] +ignore=+/home/jakob/src/live-images + +[monthly] +ignore=+/home/jakob/src/live-images + +[yearly] +ignore=+/home/jakob/src/live-images diff --git a/files/etc/mkbackup-btrfs.conf.d/thunderbird-imap.conf b/files/etc/mkbackup-btrfs.conf.d/thunderbird-imap.conf new file mode 100644 index 0000000..82da817 --- /dev/null +++ b/files/etc/mkbackup-btrfs.conf.d/thunderbird-imap.conf @@ -0,0 +1,2 @@ +[DEFAULT] +ignore=+\.icedove|thunderbird/.*/ImapMail diff --git a/files/etc/systemd/system/scripts/btrfs-action.sh b/files/etc/systemd/system/scripts/btrfs-action.sh new file mode 100755 index 0000000..31786d2 --- /dev/null +++ b/files/etc/systemd/system/scripts/btrfs-action.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +DEVICE="$1"; shift +ACTION="$1"; shift +MAINPID="$1" + +[ x"$MAINPID" = "x" ] && exit 0 +/bin/ps h -o command -p "$MAINPID" && /sbin/btrfs $ACTION cancel "$DEVICE" || exit 0 diff --git a/files/etc/systemd/system/scripts/mksnapshot-create-volume.sh b/files/etc/systemd/system/scripts/mksnapshot-create-volume.sh new file mode 100755 index 0000000..e90c92b --- /dev/null +++ b/files/etc/systemd/system/scripts/mksnapshot-create-volume.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +# Create udev-rule and mount-entry for new backup-volume + +ACTION=$1 +case $2 in + -u) + UUID=$3 + DEV=$(readlink -f /dev/disk/by-uuid/$UUID) + PARTUUID="$(blkid /dev/disk/by-uuid/$UUID -o value -s PARTUUID)" + ;; + u-*) + UUID=${2#u-} + DEV=$(readlink -f /dev/disk/by-uuid/$UUID) + PARTUUID="$(blkid /dev/disk/by-uuid/$UUID -o value -s PARTUUID)" + ;; + -p) + PARTUUID=$3 + DEV=$(readlink -f /dev/disk/by-partuuid/$PARTUUID) + UUID="$(blkid /dev/disk/by-partuuid/$PARTUUID -o value -s UUID)" + ;; + p-*) + PARTUUID=${2#p-} + DEV=$(readlink -f /dev/disk/by-partuuid/$PARTUUID) + UUID="$(blkid /dev/disk/by-partuuid/$PARTUUID -o value -s UUID)" + ;; + d-*) + DEV=${2#d-} + UUID="$(blkid $DEV -o value -s UUID)" + PARTUUID="$(blkid $DEV -o value -s PARTUUID)" + ;; + *) + DEV="$(/bin/systemd-escape -p -u $2)" + UUID="$(blkid $DEV -o value -s UUID)" + #PARTUUID="$(blkid $DEV -o value -s PARTUUID)" + PRE="d-" + ;; +esac + +#DESTUDEV="/tmp/" +#DESTSYSTEMD="/tmp/" +DESTUDEV="/etc/udev/rules.d/" +DESTSYSTEMD="/etc/systemd/system/" + +SYSTEMCTL="/bin/systemctl" + +echo "$ACTION ${DEV} | ${UUID} | ${PARTUUID}" + +sleep 1 + +if [ "$DEV"x = "x" ] +then + TYPE="btrfs" +else + TYPE="$(blkid $DEV -o value -s TYPE)" + echo "T $TYPE | $DEV" +fi + +if [ "$PARTUUID"x = "x" ]; then + if [ "$UUID"x = "x" ]; then + echo "$PARTUUID | $UUID | $DEV is no valid device" + exit 3 + else + DUUID="$UUID" #DUUID is uuid which is taken to use + SUUID="ID_FS_UUID" #SUUID is the string for the udev-rule it's UUID or PARTUUID + ID="uuid" #ID is also for the udev-rule. To look in /dev/disk/by-uuid or /dev/disk/by-partuuid + PRE="u-" + fi +else + DUUID="$PARTUUID" + SUUID="ID_PART_ENTRY_UUID" + ID="partuuid" + PRE="p-" +fi + +#echo "$DUUID | $SUUID | $ID | $PRE" +# Start by udev +start () { + +mkdir -p "${DESTSYSTEMD}var-cache-backup.mount.d/" + +echo "[Mount] +What=/dev/disk/by-${ID}/${DUUID} +" > "${DESTSYSTEMD}var-cache-backup.mount.d/source.conf" + +$SYSTEMCTL daemon-reload + +} + +# Create udev-Rule for new external drive +register () { +#echo "$UUID" +#echo "AAA $(/bin/systemd-escape /dev/disk/by-${ID}/${DUUID}|sed -e 's@\\@\\\\@g')" +#echo "ACTION==\"add\", KERNEL==\"sd*\", SUBSYSTEMS==\"usb\", ENV{${SUUID}}==\"$DUUID\", SYMLINK+=\"disk/mars\", TAG+=\"systemd\", ENV{SYSTEMD_WANTS}+=\"mkbackup-external@${PRE}${DUUID}.service\", ENV{SYSTEMD_WANTS}+=\"mkbackup@BKP.target\", ENV{SYSTEMD_WANTS}+=\"smartctl-fast@$(/bin/systemd-escape /dev/disk/by-${ID}/${DUUID}|sed -e 's@\\@\\\\@g').service\" +echo "ACTION==\"add\", KERNEL==\"sd*\", SUBSYSTEMS==\"usb\", ENV{${SUUID}}==\"$DUUID\", SYMLINK+=\"disk/mars\", TAG+=\"systemd\", ENV{SYSTEMD_WANTS}+=\"mkbackup-external@${PRE}${DUUID}.service\", ENV{SYSTEMD_WANTS}+=\"mkbackup@BKP.target\", ENV{SYSTEMD_WANTS}+=\"smartctl-fast@$(/bin/systemd-escape /dev/disk/by-${ID}/${DUUID}).service\" + +ACTION==\"remove\", KERNEL==\"sd*\", SUBSYSTEMS==\"usb\", ENV{${SUUID}}=\"$DUUID\", \ +RUN+=\"${SYSTEMCTL} --no-block stop mkbackup@BKP.target\"" > "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules" +} + + +# delete udev-rule, if external drive is not longer in use for backups. +unregister () { +[ -e "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules" ] && rm "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules" +} + +case $TYPE in + btrfs) + ;; + *) + echo "$DEV isn't a btrfs-filesystem. Exiting"; exit 1;; +esac + +case $ACTION in + register) + #setup udev-rule for device + register + ;; + unregister) + #delete udev-rule for device + unregister ;; + start) + #activate device + start;; + stop) + #deactivate device + stop ;; + *) + echo "$ACTION not recognized"; + exit 2;; +esac + +$SYSTEMCTL daemon-reload +#/bin/systemctl +exit 0 diff --git a/files/etc/systemd/system/set-environ.service b/files/etc/systemd/system/set-environ.service new file mode 100644 index 0000000..c78ddcd --- /dev/null +++ b/files/etc/systemd/system/set-environ.service @@ -0,0 +1,9 @@ +[Unit] +Description=Set Environment syssubvol for systemd + +[Service] +Type=simple +ExecStart=/bin/sh -c '/bin/systemctl set-environment SYSSUBVOL=`/usr/bin/syssubvol`' + +[Install] +WantedBy=basic.target diff --git a/files/etc/systemd/system/systemd-journald.service.d/unit.conf b/files/etc/systemd/system/systemd-journald.service.d/unit.conf new file mode 100644 index 0000000..280919b --- /dev/null +++ b/files/etc/systemd/system/systemd-journald.service.d/unit.conf @@ -0,0 +1,2 @@ +[Unit] +RequiresMountsFor=/var/log diff --git a/files/lib/systemd/system/backup.automount b/files/lib/systemd/system/backup.automount new file mode 100644 index 0000000..6adcc3d --- /dev/null +++ b/files/lib/systemd/system/backup.automount @@ -0,0 +1,13 @@ +[Unit] +Description=Automounting of /backup - activated by backup.path +#Before=local-fs.target remote-fs.target +#DefaultDependencies = yes +After=backup.path + +[Automount] +TimeoutIdleSec=15s +Where=/backup + + +#[Install] +#WantedBy=mkbackup.target diff --git a/files/lib/systemd/system/backup.mount b/files/lib/systemd/system/backup.mount new file mode 100644 index 0000000..49efb81 --- /dev/null +++ b/files/lib/systemd/system/backup.mount @@ -0,0 +1,11 @@ +[Unit] +Description=Mounts Backup-Snapshots from System to /backup +After=local-fs.target +#BindsTo=backup.automount + +[Mount] +What=MksnapshotFS.py +Where=/backup +Type=fuse +TimeoutSec=10s +Options=noauto,ro,nofail diff --git a/files/lib/systemd/system/backup.path b/files/lib/systemd/system/backup.path new file mode 100644 index 0000000..81c7fc6 --- /dev/null +++ b/files/lib/systemd/system/backup.path @@ -0,0 +1,12 @@ +[Unit] +Description=Start automounter for /backup, if this directory exists +PartOf=mkbackup.target +#Before=local-fs.target +#DefaultDependencies = no + +[Path] +PathExists=/backup +Unit=backup.automount + +[Install] +WantedBy=mkbackup.target diff --git a/files/lib/systemd/system/btrfs-balance@.service b/files/lib/systemd/system/btrfs-balance@.service new file mode 100644 index 0000000..3d53813 --- /dev/null +++ b/files/lib/systemd/system/btrfs-balance@.service @@ -0,0 +1,41 @@ +[Unit] +Description=Balance btrfs-filesystem %f + +#ConditionPathExists=!/run/btrfs-actions +#ConditionDirectoryNotEmpty=!/run/btrfs-actions +#ConditionPathExists=!/run/mkbackup +#ConditionDirectoryNotEmpty=!/run/mkbackup +ConditionACPower=true + +Conflicts=sleep.target suspend.target shutdown.target +Wants=btrfs-scrub@%i.service +OnFailure=status-email-root@%n.service +Wants=status-email-root@%n.service +Before=status-email-root@%n.service + +RefuseManualStart=true + +[Service] +RuntimeDirectory=btrfs-actions +Type=oneshot +#Restart=always + +Nice=12 +IOSchedulingClass=3 +#IOSchedulingPriority= +CPUSchedulingPolicy=idle + +ExecStartPre=/bin/touch /run/btrfs-actions/balance-systemd.lock +#ExecStart=/bin/btrfs balance start -musage=0 -dusage=0 -v %f +ExecStart=/bin/btrfs balance start -musage=5 -dusage=5 -v %f +#ExecStart=/bin/btrfs balance start -musage=10 -v %f +#ExecStart=/bin/btrfs balance start -musage=40 -v %f +#ExecStart=/bin/btrfs balance start -musage=60 -v %f +#ExecStart=/bin/btrfs balance start -dusage=0 -v %f +#ExecStart=/bin/btrfs balance start -dusage=5 -v %f +#ExecStart=/bin/btrfs balance start -dusage=10 -v %f +#ExecStart=/bin/btrfs balance start -dusage=40 -v %f +#ExecStart=/bin/btrfs balance start -dusage=65 -v %f +ExecStop=/usr/lib/systemd/scripts/btrfs-action.sh %f balance $MAINPID +ExecStopPost=/bin/rm /run/btrfs-actions/balance-systemd.lock + diff --git a/files/lib/systemd/system/btrfs-scrub@.service b/files/lib/systemd/system/btrfs-scrub@.service new file mode 100644 index 0000000..3d6ed49 --- /dev/null +++ b/files/lib/systemd/system/btrfs-scrub@.service @@ -0,0 +1,35 @@ +[Unit] +#Description=Scrub btrfs-filesystem %f %i +Description=Scrubs filesystem on %f weekly +Wants=btrfs-balance@%i.service +After=btrfs-balance@%i.service + +#ConditionPathExists=!/run/btrfs-actions +#ConditionDirectoryNotEmpty=!/run/btrfs-actions +#ConditionPathExists=!/run/mkbackup +#ConditionDirectoryNotEmpty=!/run/mkbackup +#ConditionACPower=true + + +#Wants=btrfs-status-email-root@%i.service +#Before=btrfs-status-email-root@%i.service +Conflicts=sleep.target suspend.target shutdown.target +OnFailure=status-email-root@%n.service + +[Service] +RuntimeDirectory=btrfs-actions +#Type=oneshot + +Nice=11 +IOSchedulingClass=3 +#IOSchedulingPriority= +CPUSchedulingPolicy=idle + +ExecStartPre=/bin/touch /run/btrfs-actions/scrub-systemd-%i.lock +ExecStart=/bin/btrfs scrub start -Bd %f +ExecStop=/usr/lib/systemd/scripts/btrfs-action.sh %f scrub $MAINPID +ExecStopPost=/bin/rm /run/btrfs-actions/scrub-systemd-%i.lock + + +[Install] +WantedBy=timer-weekly.target diff --git a/files/lib/systemd/system/btrfs-status-email-root@.service b/files/lib/systemd/system/btrfs-status-email-root@.service new file mode 100644 index 0000000..6323054 --- /dev/null +++ b/files/lib/systemd/system/btrfs-status-email-root@.service @@ -0,0 +1,8 @@ +[Unit] +Description=btrfs-status email for %f to root + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/systemd-btrfs-email root@localhost %i %f +#User=nobody +Group=systemd-journal diff --git a/files/lib/systemd/system/mkbackup-conf@.path b/files/lib/systemd/system/mkbackup-conf@.path new file mode 100644 index 0000000..b4d439f --- /dev/null +++ b/files/lib/systemd/system/mkbackup-conf@.path @@ -0,0 +1,11 @@ +[Unit] +Description=Upate /tmp/%I.conf.tmp if /etc/%I* changes + +[Path] +PathModified=/etc/%I.conf +PathModified=/etc/%I.conf.d/ +PathChanged=/tmp/%I.conf.tmp +Unit=mkbackup-conf@%i.service + +[Install] +WantedBy=paths.target diff --git a/files/lib/systemd/system/mkbackup-conf@.service b/files/lib/systemd/system/mkbackup-conf@.service new file mode 100644 index 0000000..9dc65c1 --- /dev/null +++ b/files/lib/systemd/system/mkbackup-conf@.service @@ -0,0 +1,9 @@ +[Unit] +Description=Create or update temporary conf-file /tmp/%I.conf.tmp for shell-extension + +[Service] +#ExecStart=/bin/sh -c "(/usr/local/bin/mkbackup list -i --print-config > /tmp/%I.conf.tmp)" +ExecStart=/usr/local/bin/mkbackup list -i --print-config -o /tmp/%I.conf.tmp + +[Install] +WantedBy=multi-user.target diff --git a/files/lib/systemd/system/mkbackup-external@.service b/files/lib/systemd/system/mkbackup-external@.service new file mode 100644 index 0000000..6dd57b2 --- /dev/null +++ b/files/lib/systemd/system/mkbackup-external@.service @@ -0,0 +1,8 @@ +[Unit] +Description=Activate external Drive/Partition for mkbackup for device %i +Wants=mkbackup@BKP.target +Before=mkbackup@BKP.target + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/scripts/mksnapshot-create-volume.sh start %i diff --git a/files/lib/systemd/system/mkbackup-register@.service b/files/lib/systemd/system/mkbackup-register@.service new file mode 100644 index 0000000..d03c6b5 --- /dev/null +++ b/files/lib/systemd/system/mkbackup-register@.service @@ -0,0 +1,6 @@ +[Unit] +Description=Activate external Drive/Partition for mkbackup/mksnapshot for device %I. + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/scripts/mksnapshot-create-volume.sh register %i diff --git a/files/lib/systemd/system/mkbackup-unregister@.service b/files/lib/systemd/system/mkbackup-unregister@.service new file mode 100644 index 0000000..1016a9c --- /dev/null +++ b/files/lib/systemd/system/mkbackup-unregister@.service @@ -0,0 +1,6 @@ +[Unit] +Description=Deactivate external Drive/Partition for mkbackup/mksnapshot for device %I. + +[Service] +Type=oneshot +ExecStart=/usr/lib/systemd/scripts/mksnapshot-create-volume.sh unregister %i diff --git a/files/lib/systemd/system/mkbackup.target b/files/lib/systemd/system/mkbackup.target new file mode 100644 index 0000000..5fc9ebc --- /dev/null +++ b/files/lib/systemd/system/mkbackup.target @@ -0,0 +1,6 @@ +[Unit] +Description=Enables all backup.services + + +[Install] +WantedBy=basic.target diff --git a/files/lib/systemd/system/mkbackup@.path b/files/lib/systemd/system/mkbackup@.path new file mode 100644 index 0000000..c938141 --- /dev/null +++ b/files/lib/systemd/system/mkbackup@.path @@ -0,0 +1,13 @@ +[Unit] +Description=Activate backup on Device %i + +[Path] +PathExists=/dev/disk/by-partuuid/%i +#PathExists=/dev/disk/by-uuid/%i +#Unit=systemd-cryptsetup@mars.service +#Unit=status-email-jakob@%n.service +Unit=var-cache-backup@%i.mount +#Unit=dev-disk-by\x2dpartuuid-%i.mount + +[Install] +WantedBy=paths.target diff --git a/files/lib/systemd/system/mkbackup@.service b/files/lib/systemd/system/mkbackup@.service new file mode 100644 index 0000000..bc100da --- /dev/null +++ b/files/lib/systemd/system/mkbackup@.service @@ -0,0 +1,44 @@ +[Unit] +Description=Make %i-backup from system-subvolume + +#ConditionPathExists=!/run/btrfs-actions +#ConditionDirectoryNotEmpty=!/run/btrfs-actions +#ConditionPathExists=!/run/mkbackup +ConditionPathExistsGlob=!/run/mkbackup/* +#ConditionPathExists=!/run/btrfs-actions +#ConditionDirectoryNotEmpty=!/run/mkbackup + +Requires=set-environ.service +Wants=dpkg-get-selection.service +#Wants=mkbackup-transfer.service +After=set-environ.service dpkg-get-selection.service mkbackup.target +#After=set-environ.service dpkg-get-selection.service +#Before=mkbackup-transfer.service + +Conflicts=suspend.target shutdown.target sleep.target +OnFailure=status-email-root@%n.service + +[Service] +#Type=oneshot +#Type=simple +Type=idle +#EnvironmentFile=/etc/mkbtrbackup.conf.d/%i.conf + +#BusName=at.xundeenergie.mkbackup +RuntimeDirectory=mkbackup + +Nice=10 +IOSchedulingClass=3 +#IOSchedulingPriority= +CPUSchedulingPolicy=idle + +ExecStartPre=/bin/sh -c "(/bin/systemctl is-active -q mkbackup.target)" +#ExecStartPre=/bin/touch /run/mkbackup-%i/systemd.lock +ExecStart=/usr/local/bin/mkbackup -V -v -t %i create SNP +#ExecStopPost=-/bin/rm /run/mkbackup-%i/systemd.lock + +KillMode=mixed + +[Install] +WantedBy=timer-%i.target +DefaultInstance=mkbackup@manually.service diff --git a/files/lib/systemd/system/mkbackup@BKP.target b/files/lib/systemd/system/mkbackup@BKP.target new file mode 100644 index 0000000..96ef6ca --- /dev/null +++ b/files/lib/systemd/system/mkbackup@BKP.target @@ -0,0 +1,5 @@ +[Unit] +Description=Target for services for %i in mkbackup + +Documentation=man:systemd.special(7) +StopWhenUnneeded=no diff --git a/files/lib/systemd/system/mkbackup@SNP.target b/files/lib/systemd/system/mkbackup@SNP.target new file mode 100644 index 0000000..446ccf6 --- /dev/null +++ b/files/lib/systemd/system/mkbackup@SNP.target @@ -0,0 +1,8 @@ +[Unit] +Description=Target for services for %i in mkbackup + +Documentation=man:systemd.special(7) +StopWhenUnneeded=no + +[Install] +WantedBy=local-fs.target diff --git a/files/lib/systemd/system/smartctl-fast@.service b/files/lib/systemd/system/smartctl-fast@.service new file mode 100644 index 0000000..d9f5763 --- /dev/null +++ b/files/lib/systemd/system/smartctl-fast@.service @@ -0,0 +1,5 @@ +[Unit] +Description=Smartctl Health-Test for %f + +[Service] +ExecStart=/usr/sbin/smartctl -H %f diff --git a/files/lib/systemd/system/timer-aptupgrade.target b/files/lib/systemd/system/timer-aptupgrade.target new file mode 100644 index 0000000..e668163 --- /dev/null +++ b/files/lib/systemd/system/timer-aptupgrade.target @@ -0,0 +1,3 @@ +[Unit] +Description=Triggered by apt +StopWhenUnneeded=yes diff --git a/files/lib/systemd/system/timer-aptupgrade.timer b/files/lib/systemd/system/timer-aptupgrade.timer new file mode 100644 index 0000000..d6e4ce3 --- /dev/null +++ b/files/lib/systemd/system/timer-aptupgrade.timer @@ -0,0 +1,15 @@ +[Unit] +Description=Runs backup %I before system-update/upgrade with apt +#BindsTo=mkbackup@BKP.target +PartOf=mkbackup@SNP.target +OnFailure=status-email-root@%n.service +BindsTo=timer-xe.target + +[Timer] +OnActiveSec=1s +AccuracySec=2s +Unit=timer-aptupgrade.target +RemainAfterElapse=false + +[Install] +WantedBy=mkbackup@SNP.target diff --git a/files/lib/systemd/system/timer-plugin.target b/files/lib/systemd/system/timer-plugin.target new file mode 100644 index 0000000..c26ba49 --- /dev/null +++ b/files/lib/systemd/system/timer-plugin.target @@ -0,0 +1,3 @@ +[Unit] +Description=Target after successfully plugging in external backup-drive +StopWhenUnneeded=yes diff --git a/files/lib/systemd/system/timer-plugin.timer b/files/lib/systemd/system/timer-plugin.timer new file mode 100644 index 0000000..ab3abe1 --- /dev/null +++ b/files/lib/systemd/system/timer-plugin.timer @@ -0,0 +1,16 @@ +[Unit] +Description=Runs backup %I after pluggin in external HD +BindsTo=mkbackup@BKP.target +#PartOf=mkbackup@BKP.target +OnFailure=status-email-root@%n.service +RefuseManualStart=yes +RefuseManualStop=yes + +[Timer] +OnActiveSec=30s +AccuracySec=10min +Unit=timer-plugin.target +RemainAfterElapse=false + +[Install] +WantedBy=mkbackup@BKP.target diff --git a/files/lib/systemd/system/umount-notify@.service b/files/lib/systemd/system/umount-notify@.service new file mode 100644 index 0000000..595fed3 --- /dev/null +++ b/files/lib/systemd/system/umount-notify@.service @@ -0,0 +1,5 @@ +[Unit] +Description=dbus-notification after umount from %I + +[Service] +ExecStart=/usr/bin/dbus-send --system /at/xundeenergie/notifications at.xundeenergie.notifications.Notification string:"External Disk" string:"%I unmounted" string:"Platte kann ausgesteckt werden" diff --git a/files/lib/systemd/system/var-cache-backup.automount b/files/lib/systemd/system/var-cache-backup.automount new file mode 100644 index 0000000..3ebd5f2 --- /dev/null +++ b/files/lib/systemd/system/var-cache-backup.automount @@ -0,0 +1,17 @@ + +# Automatically generated by systemd-fstab-generator + +[Unit] +Documentation=man:fstab(5) man:systemd-fstab-generator(8) +DefaultDependencies=no +Conflicts=umount.target +Before=umount.target +#BindsTo=mkbackup@BKP.target +PartOf=mkbackup@BKP.target + +[Automount] +Where=/var/cache/backup +TimeoutIdleSec=10s + +[Install] +WantedBy=mkbackup@BKP.target diff --git a/files/lib/systemd/system/var-cache-backup.mount b/files/lib/systemd/system/var-cache-backup.mount new file mode 100644 index 0000000..089cd22 --- /dev/null +++ b/files/lib/systemd/system/var-cache-backup.mount @@ -0,0 +1,18 @@ +[Unit] +SourcePath=/etc/fstab +Documentation=man:fstab(5) man:systemd-fstab-generator(8) +#Requires=var-cache-backup.automount +#After=var-cache-backup.automount +PartOf=var-cache-backup.automount +#Wants=backup.mount backup.automount +#Before=backup.mount +#Before=umount-notification@%p.service +#Wants=umount-notification@%P.service + +[Mount] +#What - drop-in in var-cache-backup.mount.d by mkbackup-start@...service which is started by udev-rule +#What=/dev/disk/by-uuid/e75ad421-9cda-4031-a61c-4e99bc882e1c +Where=/var/cache/backup +Type=btrfs +TimeoutSec=10s +Options=noauto,user,noatime,nofail,compress=lzo,space_cache,noinode_cache,relatime,subvol=/ diff --git a/files/usr/bin/MksnapshotFS.py b/files/usr/bin/MksnapshotFS.py new file mode 100755 index 0000000..4cf8f1c --- /dev/null +++ b/files/usr/bin/MksnapshotFS.py @@ -0,0 +1,459 @@ +#!/usr/bin/python -u +#!/usr/bin/env python + +# Copyright (C) 2001 Jeff Epler +# Copyright (C) 2006 Csaba Henk +# +# This program can be distributed under the terms of the GNU LGPL. +# See the file COPYING. +# + +import os, sys +from errno import * +from stat import * +import fcntl +# pull in some spaghetti to make this stuff work without fuse-py being installed +try: + import _find_fuse_parts +except ImportError: + pass +import fuse +from fuse import Fuse + +# JS - Snapshotnamehandling, get the current user and so on +import getpass +import socket + +from time import strptime, strftime +from datetime import datetime,date,time,timedelta + +#from mksnapshotconfig import * +from mkbackup.mkbackup_btrfs_config import * + +DEBUG = False + +if not hasattr(fuse, '__version__'): + raise RuntimeError, \ + "your fuse-py doesn't know of fuse.__version__, probably it's too old." + +fuse.fuse_python_api = (0, 2) + +fuse.feature_assert('stateful_files', 'has_init') + +def flag2mode(flags): + md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'} + m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)] + + if flags | os.O_APPEND: + m = m.replace('w', 'a', 1) + + return m + + +class Xmp(Fuse): + + def __init__(self, *args, **kw): + + Fuse.__init__(self, *args, **kw) + + if DEBUG: + for i in args: print "ARG: "+i + for i in kw: print "KW: "+i + + #get hostname + # die auskommentierte if-Funktion liefert hostname.localdomain. +# if socket.gethostname().find('.')>=0: +# hostname=socket.gethostname() +# else: +# hostname=socket.gethostbyaddr(socket.gethostname())[0] + #socket.gethostname() liefert nur "hostname" ohne .localdomain + hostname=socket.gethostname() + + # do stuff to set up your filesystem here, if you want + #import thread + #thread.start_new_thread(self.mythread, ()) + + CONFIG=Config() + + self.root=CONFIG.getStorePath('SNP').encode() + self.local=CONFIG.getStorePath('SNP').encode() + self.extern=CONFIG.getStorePath('BKP').encode() + self.snapshots={} + self.direntries=[] + Xmp.realpath="" + self.USER=getpass.getuser() + if os.getuid() == 0: + self.uroot = True + else: + self.uroot = False + + try: + self.HOME=os.environ['HOME'] + except: + self.HOME = None + self.uroot = True + +# JS - some helpers + def translate(self,subvolname,location='loc'): + n = len(subvolname.split('.')) + ac = "" + sn = subvolname.split('.')[0]+'.' + if n == 1: + #return subvolname+'--'+location + return 'CURRENT--'+location + elif n == 2: + return subvolname+'--'+location + elif n == 3: + try: + sndt = datetime.strptime(subvolname.split('.')[1],"%Y-%m-%d_%H:%M:%S") #snapshot-timestamp + except: + return subvolname+'--'+location + action = subvolname.split('.')[2] + if self.uroot: + ac='-'+action + else: + sn = '' + if action == 'manually': + ac="-"+action + elif action == 'aptupgrade': + ac="-"+action + else: + ac="" + + snd = datetime.date(sndt) #snapshot-date + if snd == datetime.date(datetime.now()): + #today + dts = sn+'heute_%H-%M-%S' + elif snd == datetime.date(datetime.now()-timedelta(days=1)): + #yesterday + dts = sn+'gestern_%H-%M-%S' +# elif snd == datetime.date(datetime.now()-timedelta(days=2)): +# #day before yesterday +# dts = 'vorgestern %H-%M-%S' + else: + #any day else + dts = sn+'%Y.%m.%d_%H-%M-%S' + if self.uroot: + return datetime.strftime(sndt,dts)+ac+'--'+location + else: + return datetime.strftime(sndt,dts)+'--'+location+ac + else: + return subvolname+'--'+location + + def __lsnapshots(self): + if DEBUG: print "ls snapshots" + self.snapshots.clear() + for location in self.BDIRS.iterkeys(): + if DEBUG == True: print 'Scan location '+location+': '+self.BDIRS[location] + try: + dirents = os.listdir(self.BDIRS[location]) + except: + continue + if self.uroot: + for entry in dirents: + if os.path.exists(self.BDIRS[location]+'/'+entry) \ + and not os.path.islink(self.BDIRS[location]+'/'+entry): + self.snapshots[self.translate(entry,location=location)] \ + = [self.BDIRS[location], '/'+entry+'/', location] + else: + for entry in dirents: + if os.path.exists(self.BDIRS[location]+'/'+entry+self.HOME) \ + and not os.path.islink(self.BDIRS[location]+'/'+entry): + self.snapshots[self.translate(entry,location=location)] \ + = [self.BDIRS[location], '/'+entry+self.HOME+'/', location] + + + def __realpath(self,path): + ss = path.split('/')[1] + #sv = './'+'/'.join(path.split('/')[2:]) + sv = '/'.join(path.split('/')[2:]) + if path == "/" and (self.root in self.BDIRS.values()): + return self.root + elif ss in self.snapshots: + self.root = self.snapshots[ss][0] + Xmp.realpath = self.snapshots[ss][0]+self.snapshots[ss][1] + return self.snapshots[ss][0]+self.snapshots[ss][1]+sv + else: + self.root = self.BDIRS['loc'] + Xmp.realpath = self.root + path + return Xmp.realpath + + def __lsdir(self,path):# + if DEBUG: print "ls dir" + ss = path.split('/')[1] + subdir = '/'+'/'.join(path.split('/')[2:]) + dirents = ['.', '..'] + #if path == "/" and (self.root.strip('/') == self.BDIRS['loc'].strip('/') or self.root == self.BDIRS['ext'].strip('/')): + if path == "/" and (self.root in self.BDIRS.values()): + self.root = self.BDIRS['loc'] + dirents.extend(self.snapshots.keys()) + elif ss in self.snapshots or path == "./": + dirents.extend(os.listdir(self.__realpath(path))) + else: + self.root = self.BDIRS['loc'] + dirents.extend(os.listdir(path)) + return dirents + + +# def mythread(self): +# +# """ +# The beauty of the FUSE python implementation is that with the python interp +# running in foreground, you can have threads +# """ +# print "mythread: started" +# while 1: +# time.sleep(120) +# print "mythread: ticking" + + def getattr(self, path): + return os.lstat(self.__realpath(path)) + if path == "/" and (self.root in self.BDIRS.values()): + return os.lstat(self.root) + else: + return os.lstat(self.__realpath(path)) + + def readlink(self, path): + return os.readlink(self.__realpath(path)) + + def readdir(self, path, offset): + if path == "/" and (self.root in self.BDIRS.values()): + if DEBUG: print path + pass + if path == "/": + self.__lsnapshots() + for e in self.__lsdir(path): + yield fuse.Direntry(e) + + def unlink(self, path): + return -EROFS + #os.unlink("." + path) + + def rmdir(self, path): + return -EROFS + #os.rmdir("." + path) + + def symlink(self, path, path1): + return -EROFS + #os.symlink(path, "." + path1) + + def rename(self, path, path1): + return -EROFS + #os.rename("." + path, "." + path1) + + def link(self, path, path1): + return -EROFS + #os.link("." + path, "." + path1) + + def chmod(self, path, mode): + return -EROFS + #os.chmod("." + path, mode) + + def chown(self, path, user, group): + return -EROFS + #os.chown("." + path, user, group) + + def truncate(self, path, len): + return -EROFS + #f = open("." + path, "a") + #f.truncate(len) + #f.close() + + def mknod(self, path, mode, dev): + return -EROFS + #os.mknod("." + path, mode, dev) + + def mkdir(self, path, mode): + return -EROFS + #os.mkdir("." + path, mode) + + def utime(self, path, times): + os.utime(self.__realpath(path), times) + +# The following utimens method would do the same as the above utime method. +# We can't make it better though as the Python stdlib doesn't know of +# subsecond preciseness in acces/modify times. +# +# def utimens(self, path, ts_acc, ts_mod): +# os.utime("." + path, (ts_acc.tv_sec, ts_mod.tv_sec)) + + def access(self, path, mode): + if DEBUG: print "access " + path + '||'+str(mode) + if not os.access(self.__realpath(path), mode): + return -EACCES + +# This is how we could add stub extended attribute handlers... +# (We can't have ones which aptly delegate requests to the underlying fs +# because Python lacks a standard xattr interface.) +# +# def getxattr(self, path, name, size): +# val = name.swapcase() + '@' + path +# if size == 0: +# # We are asked for size of the value. +# return len(val) +# return val +# +# def listxattr(self, path, size): +# # We use the "user" namespace to please XFS utils +# aa = ["user." + a for a in ("foo", "bar")] +# if size == 0: +# # We are asked for size of the attr list, ie. joint size of attrs +# # plus null separators. +# return len("".join(aa)) + len(aa) +# return aa + + def statfs(self): + """ + Should return an object with statvfs attributes (f_bsize, f_frsize...). + Eg., the return value of os.statvfs() is such a thing (since py 2.2). + If you are not reusing an existing statvfs object, start with + fuse.StatVFS(), and define the attributes. + + To provide usable information (ie., you want sensible df(1) + output, you are suggested to specify the following attributes: + + - f_bsize - preferred size of file blocks, in bytes + - f_frsize - fundamental size of file blcoks, in bytes + [if you have no idea, use the same as blocksize] + - f_blocks - total number of blocks in the filesystem + - f_bfree - number of free blocks + - f_files - total number of file inodes + - f_ffree - nunber of free file inodes + """ + + return os.statvfs(".") + + def fsinit(self): + if DEBUG: print "fsinit" + self.BDIRS={'loc':'/'+self.local.strip('/'),'ext':'/'+self.extern.strip('/')} + os.chdir('/'+self.local.strip('/')) + self.__lsnapshots() + + + class XmpFile(object): + + def __init__(self, path, flags, *mode): + path = Xmp.realpath+'/'.join(path.split('/')[2:]) + self.fd = os.open(path, flags, *mode) + self.file = os.fdopen(self.fd, flag2mode(flags)) + + def read(self, length, offset): + self.file.seek(offset) + return self.file.read(length) + + def write(self, buf, offset): + return -EROFS + self.file.seek(offset) + self.file.write(buf) + return len(buf) + + def release(self, flags): + self.file.close() + + def _fflush(self): + if 'w' in self.file.mode or 'a' in self.file.mode: + self.file.flush() + + def fsync(self, isfsyncfile): + self._fflush() + if isfsyncfile and hasattr(os, 'fdatasync'): + os.fdatasync(self.fd) + else: + os.fsync(self.fd) + + def flush(self): + self._fflush() + # cf. xmp_flush() in fusexmp_fh.c + os.close(os.dup(self.fd)) + + def fgetattr(self): + return os.fstat(self.fd) + + def ftruncate(self, len): + return -EROFS + #self.file.truncate(len) + + def lock(self, cmd, owner, **kw): + #return -EROFS + # The code here is much rather just a demonstration of the locking + # API than something which actually was seen to be useful. + + # Advisory file locking is pretty messy in Unix, and the Python + # interface to this doesn't make it better. + # We can't do fcntl(2)/F_GETLK from Python in a platfrom independent + # way. The following implementation *might* work under Linux. + # + # if cmd == fcntl.F_GETLK: + # import struct + # + # lockdata = struct.pack('hhQQi', kw['l_type'], os.SEEK_SET, + # kw['l_start'], kw['l_len'], kw['l_pid']) + # ld2 = fcntl.fcntl(self.fd, fcntl.F_GETLK, lockdata) + # flockfields = ('l_type', 'l_whence', 'l_start', 'l_len', 'l_pid') + # uld2 = struct.unpack('hhQQi', ld2) + # res = {} + # for i in xrange(len(uld2)): + # res[flockfields[i]] = uld2[i] + # + # return fuse.Flock(**res) + + # Convert fcntl-ish lock parameters to Python's weird + # lockf(3)/flock(2) medley locking API... + op = { fcntl.F_UNLCK : fcntl.LOCK_UN, + fcntl.F_RDLCK : fcntl.LOCK_SH, + fcntl.F_WRLCK : fcntl.LOCK_EX }[kw['l_type']] + if cmd == fcntl.F_GETLK: + return -EOPNOTSUPP + elif cmd == fcntl.F_SETLK: + if op != fcntl.LOCK_UN: + op |= fcntl.LOCK_NB + elif cmd == fcntl.F_SETLKW: + pass + else: + return -EINVAL + + fcntl.lockf(self.fd, op, kw['l_start'], kw['l_len']) + + + def main(self, *a, **kw): + + self.file_class = self.XmpFile + + return Fuse.main(self, *a, **kw) + + +def main(): + + usage = """ +Userspace nullfs-alike: mirror the filesystem tree from some point on. + +""" + Fuse.fusage + + server = Xmp(version="%prog " + fuse.__version__, + usage=usage, + dash_s_do='setsingle') + + server.parser.add_option(mountopt="root", metavar="PATH", default=server.local, + help="mirror filesystem from under PATH [default: %default]") + server.parser.add_option(mountopt="local", metavar="PATH", default=server.local, + help="set path to filesystem from internal HDD/SSD under PATH [default: %default]") + server.parser.add_option(mountopt="extern", metavar="PATH", default=server.extern, + help="set path to filesystem from external HDD/SSD under PATH [default: %default]") + server.parser.add_option(mountopt="uroot", metavar="BOOL", default=server.uroot, + help="""use ist for mounting on /backup for root - + whole snapshots! BOOL [default: %default]""") + + server.parse(values=server, errex=1) + + try: + if server.fuse_args.mount_expected(): + os.chdir(server.local) + except OSError: + print >> sys.stderr, "can't enter root of underlying filesystem" + sys.exit(1) + + server.main() + + +if __name__ == '__main__': + main() diff --git a/files/usr/bin/mkbackup-gui b/files/usr/bin/mkbackup-gui new file mode 100755 index 0000000..22db0b7 --- /dev/null +++ b/files/usr/bin/mkbackup-gui @@ -0,0 +1,59 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (C) 2013 Thomas Bechtold + +# This file is part of mkbackup-gui. + +# 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. + + +#import os +#gi_typelib_path = ["/usr/lib/x86_64-linux-gnu/mkbackup-gui/girepository-1.0",] +#if 'GI_TYPELIB_PATH' in os.environ: +# gi_typelib_path.append(os.environ['GI_TYPELIB_PATH']) +#os.environ['GI_TYPELIB_PATH'] = ":".join(gi_typelib_path) + +#ld_library_path = ["/usr/lib/x86_64-linux-gnu/mkbackup-gui",] +#if 'LD_LIBRARY_PATH' in os.environ: +# ld_library_path.append(os.environ['LD_LIBRARY_PATH']) +#os.environ['LD_LIBRARY_PATH'] = ":".join(ld_library_path) + +import os +import sys +sys.path.insert(1, '/usr/lib/python3.6/site-packages') + +import gettext, locale +from gettext import gettext as _ +gettext.textdomain("mkbackup-gui") +locale.textdomain("mkbackup-gui") + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GObject +from dfeet.application import DFeetApp + + +if __name__ == "__main__": + data_dir = "/usr/share/mkbackup-gui" + #use local paths when debugging + if os.getenv("DFEET_DEBUG") is not None: + data_dir = os.path.join(os.path.dirname(__file__), "..", "data") + Gtk.IconTheme.get_default().prepend_search_path( + os.path.join(os.path.dirname(__file__), "..", "data", "icons")) + #start the application + print(data_dir) + app = MkBackup(package="mkbackup-gui", version="0.1.0", data_dir=data_dir) + sys.exit(app.run(sys.argv)) diff --git a/files/usr/bin/syssubvol b/files/usr/bin/syssubvol new file mode 100755 index 0000000..a91cb26 --- /dev/null +++ b/files/usr/bin/syssubvol @@ -0,0 +1,11 @@ +#!/usr/bin/python3 + +import sys +from mkbackup.mkbackup_btrfs_config import MountInfo + +def relpath(mp): + print(MountInfo().relpath(mp)) + +if __name__ == "__main__": + for i in sys.argv[1:]: + relpath(i) diff --git a/files/usr/lib/python2.7/dist-packages/mkbackup/__init__.py b/files/usr/lib/python2.7/dist-packages/mkbackup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/files/usr/lib/python2.7/dist-packages/mkbackup/mkbackup_btrfs_config.py b/files/usr/lib/python2.7/dist-packages/mkbackup/mkbackup_btrfs_config.py new file mode 120000 index 0000000..79d92e8 --- /dev/null +++ b/files/usr/lib/python2.7/dist-packages/mkbackup/mkbackup_btrfs_config.py @@ -0,0 +1 @@ +../../../python3/dist-packages/mkbackup/mkbackup_btrfs_config.py \ No newline at end of file diff --git a/files/usr/lib/python2.7/dist-packages/mkbackup/mkbackup_emitter.py b/files/usr/lib/python2.7/dist-packages/mkbackup/mkbackup_emitter.py new file mode 120000 index 0000000..feedd28 --- /dev/null +++ b/files/usr/lib/python2.7/dist-packages/mkbackup/mkbackup_emitter.py @@ -0,0 +1 @@ +../../../python3/dist-packages/mkbackup/mkbackup_emitter.py \ No newline at end of file diff --git a/files/usr/lib/python2.7/dist-packages/mkbackup/system_notification_emitter.py b/files/usr/lib/python2.7/dist-packages/mkbackup/system_notification_emitter.py new file mode 120000 index 0000000..c099650 --- /dev/null +++ b/files/usr/lib/python2.7/dist-packages/mkbackup/system_notification_emitter.py @@ -0,0 +1 @@ +../../../python3/dist-packages/mkbackup/system_notification_emitter.py \ No newline at end of file diff --git a/files/usr/lib/python2.7/dist-packages/system_notification_emitter.py b/files/usr/lib/python2.7/dist-packages/system_notification_emitter.py new file mode 100644 index 0000000..8e4b870 --- /dev/null +++ b/files/usr/lib/python2.7/dist-packages/system_notification_emitter.py @@ -0,0 +1,38 @@ +"""Emmitter functionality.""" +import dbus +import dbus.service +import dbus.glib + + +class Emitter(dbus.service.Object): + """Emitter DBUS service object.""" + + def __init__(self, conn=None, object_path=None, bus_name=None): + """Initialize the emitter DBUS service object.""" + dbus.service.Object.__init__(self, conn=conn, object_path=object_path) + + @dbus.service.signal(dbus_interface='at.xundeenergie.Notification') + def low(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a low test signal') + + @dbus.service.signal(dbus_interface='at.xundeenergie.Notification') + def normal(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a normal test signal') + + @dbus.service.signal(dbus_interface='at.xundeenergie.Notification') + def critical(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a critical test signal') + +""" Example to use +Simple_Notification = Emitter(dbus.SystemBus(), + '/at/xundeenergie/notifications/simple/Notification') +Advanced_Notification = Emitter(dbus.SystemBus(), + '/at/xundeenergie/notifications/advanced/Notification') + +#Simple_Notification.low('M') +Advanced_Notification.normal( + {'sender': 'emitter1.py', 'header': 'Testmessage', 'body': 'Test Body'}) +""" diff --git a/files/usr/lib/python3/dist-packages/mkbackup-dbus/service b/files/usr/lib/python3/dist-packages/mkbackup-dbus/service new file mode 100755 index 0000000..ead1e01 --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup-dbus/service @@ -0,0 +1,35 @@ +#!/usr/bin/python3 -d +#!/usr/bin/env python3 + +import dbus, dbus.service, dbus.exceptions +import sys + +from dbus.mainloop.glib import DBusGMainLoop +from gi.repository import GLib + +# Initialize a main loop +DBusGMainLoop(set_as_default=True) +loop = GLib.MainLoop() + +# Declare a name where our service can be reached +try: + sysbus_name = dbus.service.BusName("at.xundeenergie", + bus=dbus.SystemBus(), + do_not_queue=True) +except dbus.exceptions.NameExistsException: + print("service is already running") + sys.exit(1) + +# Run the loop +try: + # Create our initial objects + from services.mkbackup import MkBackupDBus + MkBackupDBus(sysbus_name, "/at/xundeenergie/mkbackup/Intervals") + + loop.run() +except KeyboardInterrupt: + print("keyboard interrupt received") +except Exception as e: + print("Unexpected exception occurred: '{}'".format(str(e))) +finally: + loop.quit() diff --git a/files/usr/lib/python3/dist-packages/mkbackup-dbus/services/__init__.py b/files/usr/lib/python3/dist-packages/mkbackup-dbus/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/files/usr/lib/python3/dist-packages/mkbackup-dbus/services/mkbackup.py b/files/usr/lib/python3/dist-packages/mkbackup-dbus/services/mkbackup.py new file mode 100644 index 0000000..edfad8b --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup-dbus/services/mkbackup.py @@ -0,0 +1,117 @@ +import dbus +import dbus.service +import random +import time +import os + +from mkbackup_btrfs_config import Config, MountInfo, connect, Myos, __version__ + +config = Config() + +class MkBackup: + def __init__(self, bus_name, base_path): +# self._bus = dbus.SystemBus() +# self.notification = Notification() + Intervals(bus_name, base_path) + + for intv in config.ListIntervals(): + Properties(bus_name, os.path.join(base_path, intv), intv) + +class Intervals(dbus.service.Object): + def __init__(self, bus_name, bus_path): + super().__init__(bus_name, bus_path) + + @dbus.service.method(dbus_interface='at.xundeenergie.mkbackup.Intervals', + in_signature='', out_signature='v') + def Names(self): + return config.ListIntervals() + +class Properties(dbus.service.Object): + def __init__(self, bus_name, bus_path, interval): + super().__init__(bus_name, bus_path) + self.interface = "at.xundeenergie.mkbackup.Status" + self.interval = interval + self.STATI = ['reset', 'stop', 'running', 'finished'] + self.properties = dict() + self.properties[self.interface] = dict() + self.properties[self.interface]['progress'] = 0 # 0-100 + self.properties[self.interface]['status'] = 'stop' # stop, running, finished, reset + self.properties[self.interface]['transfer'] = config.getTransfer(interval) + self.properties[self.interface]['lastrun'] = 0 # datetime + self.properties[self.interface]['finished'] = True # Boolean + self.properties[self.interface]['name'] = interval # Boolean + self.properties['function'] = dict() + self.properties['function']['progress'] = self.update_progress + self.properties['function']['status'] = self.update_status +# self.properties['function']['lastrun'] = self.update_lastrun + from dbus import Interface + + def update_progress(self, interface, incr): + print("Update progress: %i / %i, %s" % (float(incr), + self.properties[interface]['progress'],self.properties[interface]['status'])) + if self.properties[interface]['status'] == 'running': + if 0 < self.properties[interface]['progress'] + float(incr) < 100: + #self.properties[interface]['progress'] = self.properties[interface]['progress'] + float(incr) + self.properties[interface]['progress'] += float(incr) + elif self.properties[interface]['progress'] + float(incr) >= 100: + self.properties[interface]['progress'] = 99 + else: + print('B', incr, type(incr)) + return self.properties[interface]['progress'] + + def update_status(self, interface, status): + if status in self.STATI: + print("Update status: %s" % status) + print("Status: ", self.properties[interface]['status']) + if status == 'finished': + self.properties[interface]['status'] = status + self.properties[interface]['progress'] = 100 + elif status == 'stop': + self.properties[interface]['status'] = status + self.properties[interface]['progres'] = 0 + elif status == 'reset': + self.properties[interface]['status'] = 'running' + self.properties[interface]['progress'] = 0 + print("status: ", self.properties[interface]['status']) + return self.properties[interface]['status'] + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='ss', out_signature='v') + def Get(self, interface_name, property_name): + return self.GetAll(interface_name)[property_name] + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='s', out_signature='a{sv}') + def GetAll(self, interface_name): + if interface_name == self.interface: + return self.properties[interface_name] + else: + raise dbus.exceptions.DBusException( + 'at.xundeenergie.mkbackup.UnknownInterface', + 'The Foo object does not implement the %s interface' + % interface_name) + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='ssv') + def Set(self, interface_name, property_name, new_value): + # validate the property name and value, update internal state… + """https://recalll.co/ask/v/topic/D-Bus-D-Feet-Send-Dictionary-of-String%2CVariants-in-Python-Syntax/5565e1372bd273d7108b7b82 + __import__('gi.repository.GLib', globals(), locals(), ['Variant']).Variant("s", "value")""" + if interface_name in self.properties: + if property_name in self.properties[interface_name]: + func = self.properties['function'].get(property_name) + new_value = func(interface_name, new_value) + #self.properties[str(interface_name)][str(property_name)] = new_value + self.PropertiesChanged(interface_name, + { property_name: new_value, 'interval': self.interval}, []) + else: + raise dbus.exceptions.DBusException( + 'at.xundeenergie.mkbackup.UnknownInterface', + 'The Foo object does not implement the %s interface' + % interface_name) + + @dbus.service.signal(dbus.PROPERTIES_IFACE, + signature='sa{sv}as') + def PropertiesChanged(self, interface_name, changed_properties, + invalidated_properties): + pass diff --git a/files/usr/lib/python3/dist-packages/mkbackup/__init__.py b/files/usr/lib/python3/dist-packages/mkbackup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/files/usr/lib/python3/dist-packages/mkbackup/__pycache__/mkbackup_emitter.cpython-36.pyc b/files/usr/lib/python3/dist-packages/mkbackup/__pycache__/mkbackup_emitter.cpython-36.pyc new file mode 100644 index 0000000..a463f6b Binary files /dev/null and b/files/usr/lib/python3/dist-packages/mkbackup/__pycache__/mkbackup_emitter.cpython-36.pyc differ diff --git a/files/usr/lib/python3/dist-packages/mkbackup/interval_watch.py b/files/usr/lib/python3/dist-packages/mkbackup/interval_watch.py new file mode 100644 index 0000000..cc1da03 --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/interval_watch.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function + +from gi.repository import GObject, Gtk, Gio +from mkbackup.uiloader import UILoader + +import os + +from dfeet.wnck_utils import IconTable + +class IntervalBox(Gtk.Box): + """class to represent a snapshot-interval""" + def __init__(self, interval): + super(IntervalBox, self).__init__(spacing=5, expand=True) + self.__interval_name = interval + self.__enabled = False + self.__icon_table = IconTable.get_instance() + self.__icon_image = Gtk.Image.new_from_pixbuf(self.__icon_table.default_icon) + + self.__hbox = Gtk.HBox(spacing=5, halign=Gtk.Align.START) + self.pack_start(self.__hbox, True, True, 0) + # icon + self.__hbox.pack_start(self.__icon_image, True, True, 0) + # other information + self.__vbox_right = Gtk.VBox(spacing=5, expand=True) + self.__hbox.pack_start(self.__vbox_right, True, True, 0) + + # first element + self.__label_interval_name = Gtk.Label() + self.__label_interval_name.set_halign(Gtk.Align.START) + self.__vbox_right.pack_start(self.__label_interval_name, True, True, 0) + # second element + self.__label_info = Gtk.Label() + self.__label_info.set_halign(Gtk.Align.START) + self.__vbox_right.pack_start(self.__label_info, True, True, 0) + # switch to enable/disable it + self.__switch_enabled = Gtk.Switch() + self.__switch_enabled.set_halign(Gtk.Align.START) + self.__switch_enabled.connect('notify::active', self.on_switch_activated) + self.__vbox_right.pack_start(self.__switch_enabled, True, True, 0) + # transfer snapshot to backup + self.__check_transfer = GtkCheckButton() + self.__check_transfer.set_active(props['transfer']) + self.__vbox_right.pack_start(self.__check_transfer, True, True, 0) + # progressbar + self.__progress = Gtk.ProgressBar() + self.__progress.set_fraction(props['progress']/100) + self.__vbox_right.pack_start(self.__progress, True, True, 0) + # separator for the boxes + self.pack_end(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True, 0) + # update widget information + self.__update_widget() + self.show_all() + + +class IntervalWatch(object): + """watch a given snapshot-interval""" + def __init__(self, interval): + self.__interval_name = interval + + # Setup ui + ui = Gtk.Builder() + ui.add_from_file("test.glade") + diff --git a/files/usr/lib/python3/dist-packages/mkbackup/mkbackup.client b/files/usr/lib/python3/dist-packages/mkbackup/mkbackup.client new file mode 100755 index 0000000..69e838e --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/mkbackup.client @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +# Take in a single optional integral argument +import sys +import argparse +import os + +arg_parser = argparse.ArgumentParser(description='Get random numbers') +arg_parser.add_argument('bits', nargs='?', default=16) +arg_parser.add_argument('-s', '--slow', action='store_true', + default=False, required=False, + help='Use the slow method') + +args = arg_parser.parse_args() + +# Encapsulate calling the Status object on the session bus with a main loop +import dbus, dbus.exceptions, dbus.mainloop.glib +import dbus.service +import threading +from gi.repository import GLib + +from time import sleep + +from mkbackup_emitter import Emitter as EM +class EmDBUS(EM): + """ Example to use + progress = Emitter(dbus.SystemBus(), + '/at/xundeenergie/mkbackup/Status') + + progress.start( + {'intv': 'hourly'}) + + progress.update( + {'intv': 'hourly', 'progr': 5}) + + progress.finished( + {'intv': 'hourly'}) + + progress.reset( + {'intv': 'hourly'}) + """ + def __init__(self, interval): + super().__init__(conn=dbus.SystemBus(), bus_name='at.xundeenergie', object_path=os.path.join('/at/xundeenergie/mkbackup/Intervals', interval)) + +parser = argparse.ArgumentParser() +args = parser.parse_args() +args.action='daily' +args.mdb = EmDBUS(args.action) +args.mdb.Reset() +args.mdb.Start() +print("RUN PROGRAMM") +steps = 20 +for i in range(0,steps): + sleep(0.5) + args.mdb.Update(100/steps) +args.mdb.Finished() diff --git a/files/usr/lib/python3/dist-packages/mkbackup/mkbackup_btrfs_config.py b/files/usr/lib/python3/dist-packages/mkbackup/mkbackup_btrfs_config.py new file mode 100644 index 0000000..894dd90 --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/mkbackup_btrfs_config.py @@ -0,0 +1,751 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) +#from builtins import * + +import sys +import subprocess +import socket +import os +import errno +import re +import paramiko + +__author__ = "Jakobus Schuerz " +__version__ = "0.04.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + + +if PY3: + from configparser import ConfigParser + from configparser import RawConfigParser, NoOptionError, NoSectionError +else: + from ConfigParser import ConfigParser + from ConfigParser import RawConfigParser, NoOptionError, NoSectionError + + + +class Error(Exception): + pass + +class NoSubvolumeError(Error): + def __init__(self): + print("ERROR - Snapshot not found" ) + pass + +class SSHConnectionError(Error): + def __init__(self): + print("ERROR - ssh-connection not available" ) + pass + +def s2bool(s): + return s.lower() in ['true','yes','y','1'] if s else False + +# quote awk-argument in ssh-command +def quote_argument(argument): + return '"%s"' % ( + argument + .replace('\\', '\\\\') + .replace('"', '\\"') + .replace('$', '\\$') + .replace('`', '\\`') + ) + +def connect(conn=None): + if conn == None: + pass + else: + if not conn['active']: + try: + #if conn['conn'].is_active(): print("Session alive") + #conn['conn'].close() + conn['conn'].connect(conn['host'],conn['port'],conn['user'],auth_timeout=10) + conn['active'] = True + #print("open connection for %s@%s" % (conn['user'], conn['host'])) +# except (paramiko.BadHostKeyException, +# paramiko.AuthenticationException, paramiko.SSHException, +# socket.gaierror, socket.error) as e: +# #print("C",e) +# #raise e +# print("No connection to host %s" % (conn['host'])) +# return(False) +# except (paramiko.BadHostKeyException, paramiko.AuthenticationException, paramiko.SSHException, socket.error) as e: +# raise e + except: + return(False) + + return(True) + raise SSHConnectionError + else: + return(True) +# try: +# #if conn['conn'].is_active(): print("Session alive") +# conn['conn'].close() +# conn['conn'].connect(conn['host'],conn['port'],conn['user']) +# print("open connection for %s@%s" % (conn['user'], conn['host'])) +# except (paramiko.BadHostKeyException, paramiko.AuthenticationException, paramiko.SSHException, socket.error) as e: +# print("C",e) +# raise e + + +class MountInfo(): + def __init__(self,mountinfo='/proc/self/mountinfo',conn=None): + self.mi = dict() + if conn == None: + mif = open(mountinfo) + else: + if connect(conn): + #conn['conn'].connect(conn['host'],conn['port'],conn['user']) + sftp_client = conn['conn'].open_sftp() + mif = sftp_client.open(mountinfo) + else: + print("Host not reachable (MountInfo): %s" % (conn['host'])) + mif = open(mountinfo) + try: + for line in mif: + if len(line.split()) == 10: + a,b,c,relpath,mntp,d,typ,fstype,dev,opts = line.split() + else: + a,b,c,relpath,mntp,d,e,typ,fstype,dev,opts = line.split() + mntp = mntp.replace('\\040',' ') + self.mi[mntp] = dict() + self.mi[mntp]['relpath'] = relpath + self.mi[mntp]['typ'] = typ + self.mi[mntp]['fstype'] = fstype + self.mi[mntp]['dev'] = dev + self.mi[mntp]['opts'] = opts + finally: + mif.close() + + def __check(self,mountpoint,attribute): + if not mountpoint[0] == '/': + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), mountpoint) + mp = mountpoint.rstrip('/')if len(mountpoint) > 1 else mountpoint + rec = False + rep = '' + if os.path.exists(mp): + try: + rp = self.mi[mp][attribute] + rep = mp + except: + rec = True + a,rep,rp = self.__check(os.path.dirname(mp),attribute) + return [rec,rep,rp] + else: + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), mp) + + def relpath(self,mountpoint): + rec,rep,mp = self.__check(mountpoint,'relpath') + #print(mountpoint,rec,rep,mp) + if rec: return mp.replace(rep,'') + mountpoint + return mp + + def fstype(self,mountpoint): + return self.__check(mountpoint,'fstype')[2] + + def typ(self,mountpoint): + return self.__check(mountpoint,'typ')[2] + + def device(self,mountpoint): + return self.__check(mountpoint,'dev')[2] + +class MyConfigParser(ConfigParser): + comment = """replace get in Configparser to give the default-option, if a + section doesn't exist, and option exists in default""" + def get(self, section, option, **kw): + try: + return ConfigParser.get(self, section, option, raw=True) + except: + return ConfigParser.get(self, 'DEFAULT', option, raw=True) + +class Myos(): + def __init__(self,dry=False): + self.dry = dry + pass + + def __run__(self,command,conn=None): + if not conn == None: + if connect(conn): + out='' + stdin, stdout, stderr = conn['conn'].exec_command(command) + for line in stdout: + out += line + return(out) + else: + print("Host not reachable (Myos): %s" % (conn['host'])) + + def stat(self,path,conn=None): + if not conn == None: + command='/usr/bin/stat' + return(self.__run__(command,conn)) + else: + return os.stat(path) + + + def path_isdir(self,path,conn=None): + #print("myos.path",os.path.exists(path)) + if not conn == None: + command='/bin/test -d %s' % (path) + return(self.__run__(command,conn)) + else: +# print("is local dir %s" % (path)) + return os.path.isdir(path) + + def path_isfile(self,path,conn=None): + if not conn == None: + command='/bin/test -f %s' % (path) + return(self.__run__(command,conn)) + else: +# print("is local file %s" % (path)) + return os.path.isfile(path) + + def path_realpath(self,path,conn=None): + #print("myos.path",os.path.exists(path)) + if not conn == None: + command='/usr/bin/realpath %s' % (path) + return(self.__run__(command,conn)) + else: +# print("local realpath for %s" % (path)) + return os.path.realpath(path) + + def path_exists(self,path,conn=None): + #print("myos.path",os.path.exists(path)) + if not conn == None: + command='/bin/test -e %s' % (path) + return(self.__run__(command,conn)) + else: +# print("exists-local %s" % (path)) + return os.path.exists(path) + + def remove(self,path,conn=None): + if self.dry == True: + print('Remove %s (dry run)' % (path)) + return + else: + if not conn == None: + command='/bin/rm %s' % (path) + return(self.__run__(command,conn)) + else: + # print("remove-local %s" % (path)) + return os.remove(path) + + def rename(self,From,To,conn=None): + if self.dry == True: + print('Rename %s to %s (dry run)' % (From,To)) + return + else: + if not conn == None: + # print("RENAME",From,To,conn['host']) + command='/bin/mv %s %s' % (From,To) + return(self.__run__(command,conn)) + else: + # print("rename-local %s %s" % (From,To)) + return os.rename(From,To) + + + def path_islink(self,path,conn=None): + if not conn == None: + command='/bin/test -h %s' % (path) + return(self.__run__(command,conn)) + else: + return os.path.islink(path) + + def listdir(self,path,conn=None): + if not conn == None: + command='/bin/ls %s' % (path) + return(self.__run__(command,conn)) + else: + return os.listdir(path) + + +class Config(): + def __init__(self,cfile='/etc/mkbackup-btrfs.conf'): + self.cfile = cfile + #self.config = ConfigParser() + self.config = MyConfigParser() + #self.hostname = subprocess.check_output("/bin/hostname",shell=True).decode('utf8').split('\n')[0] + self.hostname=socket.gethostname() + self.mountinfo = MountInfo() + self.syssubvol = self.mountinfo.relpath('/')[1:] + #self.syssubvol=subprocess.check_output(['/usr/bin/grub-mkrelpath','/'], shell=False).decode('utf8').split("\n")[0].strip("/") + self.ssh = dict() + self.ssh_cons = dict() + + #if os.path.exists(self.cfile): + if Myos().path_exists(self.cfile): + pass #print('OK') + else: + print('Default-Config created at %s' % (self.cfile)) + self.CreateConfig() + self._read() + + for i in self.ListIntervals() +['DEFAULT']: + self.ssh[i] = dict() + for s in ['SRC', 'SNP', 'BKP']: + #print('X',self.getSSHLogin(s,i)) + self.ssh[i][s] = dict() + if self.getSSHLogin(s,i) != None: + c,x,p,uh = self.getSSHLogin(s,i).strip().split(' ') + u,h = uh.split('@') + if not uh in self.ssh_cons: + self.ssh_cons[uh] = paramiko.SSHClient() + self.ssh_cons[uh].set_missing_host_key_policy(paramiko.AutoAddPolicy()) + #self.ssh_cons[uh].connect(h, int(p), u) + + #self.ssh[i][s]['conn'] = uh + self.ssh[i][s]['conn'] = self.ssh_cons[uh] + self.ssh[i][s]['host'] = h + self.ssh[i][s]['port'] = int(p) + self.ssh[i][s]['user'] = u + self.ssh[i][s]['creds'] = (h, int(p), u) + self.ssh[i][s]['active'] = False + + else: + self.ssh[i][s] = None + + def _read(self): + + csup = dict() #for each dropin-file a csup = config-superseed-dict-entry + #self.config = ConfigParser() + self.config.read(self.cfile) + self.csupdir = self.cfile+'.d' + if os.path.exists(str(self.csupdir)) and os.path.isdir(str(self.csupdir)): + for csuplst in os.listdir(self.csupdir): + if csuplst.endswith('.conf'): + csup[csuplst] = ConfigParser() + csup[csuplst].read(self.csupdir+'/'+csuplst) + + # first superseed defaults + for i in sorted(csup.keys()): + # Set Options + for j in ['DEFAULT']: + for k in csup[i].defaults() if j == 'DEFAULT' else csup[i].options(j): + if self.config.has_section(j) or j == 'DEFAULT': + if k == 'ignore': + # only attend pattern on option 'ignore' + orig = self.config.get(j,k) + ',' if self.config.has_option(j,k) else '' + elif k == 'description': + # only attend pattern on option 'description' + #orig = self.config.get(j,k) if self.config.has_option(j,k) else '' + orig = '' + else: + # if option is not ignore, do the same as without + # +, but remove + as first character + orig = '' + self.config.set(j,k,orig + re.sub('^\+','',csup[i].get(j,k))) + #self.config.set(j,k,re.sub('^\+*','',csup[i].get(j,k))) + else: + # add section + self.config.add_section(j) + for k in csup[i].options(j): + # add option to new section + self.config.set(j,k,re.sub('^\+','',csup[i].get(j,k))) + + # second superseed normal options + for i in sorted(csup.keys()): + for j in csup[i].sections(): + for k in csup[i].defaults() if j == 'DEFAULT' else csup[i].options(j): + if self.config.has_section(j) or j == 'DEFAULT': + if k == 'ignore': + # only attend pattern on option 'ignore' + orig = self.config.get(j,k) + ',' if self.config.has_option(j,k) else '' + elif k == 'description': + # only attend pattern on option 'description' + #print(self.config.get(j,k)) + orig = self.config.get(j,k) if self.config.has_option(j,k) else '' + else: + # if option is not ignore, do the same as without + # +, but remove + as first character + orig = '' + self.config.set(j,k,orig + re.sub('^\+','',csup[i].get(j,k))) + else: + # add section + self.config.add_section(j) + for k in csup[i].options(j): + # add option to new section + self.config.set(j,k,re.sub('^\+','',csup[i].get(j,k))) + + + # If directory, where mkbackup-btrfs is started from, is one of SNP or + # BKP, set SRC to the configured path and store + psrc = os.getcwd() + if '/'+psrc.strip('/') == self.getMountPath('SNP')[1]: + #print("A",self.getMountPath('SNP')) + self.config.set('DEFAULT','SRC', ' '.join(self.getMountPath('SNP'))) + self.config.set('DEFAULT','srcstore', self.getStoreName('SNP')) + elif '/'+psrc.strip('/') == self.getMountPath('SNP')[1]+'/'+self.getStoreName('SNP'): + #print("B") + self.config.set('DEFAULT','SRC', ' '.join(self.getMountPath('SNP'))) + self.config.set('DEFAULT','srcstore', self.getStoreName('SNP')) + elif '/'+psrc.strip('/') == self.getMountPath('BKP')[1]+'/'+self.getStoreName('BKP'): + #print("C") + self.config.set('DEFAULT','SRC', ' '.join(self.getMountPath('BKP'))) + self.config.set('DEFAULT','srcstore', self.getStoreName('BKP')) + elif '/'+psrc.strip('/') == self.getMountPath('BKP')[1]: + #print("D") + self.config.set('DEFAULT','SRC', ' '.join(self.getMountPath('BKP'))) + self.config.set('DEFAULT','srcstore', self.getStoreName('BKP')) + else: + #print("E") + self.config.set('DEFAULT','SRC', psrc) + self.config.set('DEFAULT','srcstore', '') + +# for i in self.config.sections(): +# print('XX[%s]' %(i)) +# for j in self.config.options(i): +# print("XX"+j+' = ',self.__trnName(self.config.get(i,j))) +# print('') + + def getssh(self,tag,store): + tg = tag if tag in self.ssh else 'DEFAULT' + return(self.ssh[tg][store]) + + def getSsh(self,tag): + tg = tag if tag in self.ssh else 'DEFAULT' + return(self.ssh[tg]) + + def CreateConfig(self): + self.config['DEFAULT'] = { + 'Description': "Erstellt ein Backup", + 'SNPMNT': '/var/cache/btrfs_pool_SYSTEM', + 'BKPMNT': '/var/cache/backup', + 'snpstore': '', + 'bkpstore': '$h', + 'volumes': '$S,__ALWAYSCURRENT__', + 'interval': 5, + 'symlink': 'LAST', + 'transfer': False, + 'notification': None, + 'notification_urgency': None} + self.config['hourly'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '24','transfer': True} + self.config['daily'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '7','transfer': True} + self.config['weekly'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '5','transfer': True} + self.config['monthly'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '12','transfer': True} + self.config['yearly'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '7','transfer': True} + self.config['afterboot'] = {'volumes': '$S','interval': '4','symlink': 'LASTBOOT'} + self.config['aptupgrade'] = {'volumes': '$S','interval': '6','symlink': 'BEFOREUPDATE'} + self.config['dmin'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '6'} + self.config['plugin'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': + '5','transfer': True, 'notification': 'desktop', 'notification_urgency': 1} + self.config['manually'] = {'volumes': '$S,__ALWAYSCURRENT__','interval': '5','symlink': 'MANUALLY', + 'transfer': True, 'notification': 'desktop', 'notification_urgency': 2} + + with open(self.cfile, 'w') as configfile: + try: + self.config.write(configfile) + except: + exit("Failure during creation of config-file") + return(self.config) + + def PrintConfig(self,tag=None,of=None): + if tag == None: + seclist = self.config.sections() + else: + seclist = [tag] + + out = list() + if tag == None: + out.append('[DEFAULT]') + for j in self.config.defaults(): + out.append("%s = %s" % (j,self.__trnName(self.config.get('DEFAULT',j)))) + out.append('') + + #for i in self.config.sections() if tag == None else [tag]: + for i in seclist: + out.append('[%s]' %(i)) + for j in self.config.options(i): + out.append("%s = %s" % (j,self.__trnName(self.config.get(i,j)))) + out.append('') + + if of != None: + with open(of, 'w') as f: + try: + f.write('\n'.join(out)) + except: + raise + exit("Failure during creation of tmp-config-file") + else: + print('\n'.join(out)) + + + def ListIntervals(self): + LST = [] + for i in self.config.sections(): + LST.append(i) + LST.append('misc') + return(LST) + + def ListIntervalsFull(self): + #self._read() + LST = [] + for i in self.config.sections(): + LST.append(i+': '+str(self.config.get(i,'interval'))) + LST.append('misc: '+str(self.config.get(i,'interval'))) + return(LST) + + def ListSymlinkNames(self): + #self._read() + LST = [] + for i in self.config.sections(): + LST.append(self.config.get(i,'symlink')) + return(list(set(LST))) + + + def getMountPath(self, store='SRC', tag='DEFAULT', shlogin=False, original=True): + if store == 'SRC': + path = self.config.get(tag,'SRC') + elif store == 'SNP': + path = self.config.get(tag,'SNPMNT') + elif store == 'BKP': + path = self.config.get(tag,'BKPMNT') + else: + print("EE - getMountPath: store %s is not allowed (%s) set path to SRC" % (store,tag)) + path = self.config.get('DEFAULT','SRC') + #print('PATH',tag,path) + + _ssh = path.split(':') + if len(_ssh) == 1: + path = _ssh[0] + SSH = None + elif len(_ssh) == 2: + userhost,path = _ssh + port = '22' + SSH = [userhost,port] + elif len(_ssh) == 3: + userhost,port,path = _ssh + SSH = [userhost,port] + + if shlogin: + return(SSH) + else: + if original: + sshout = '' + if SSH != None: sshout=':'.join(SSH)+':' + return(sshout+'/'+path.strip('/')) + else: + return('/'+path.strip('/')) + + # avoid deleting of / - but it's buggy, so return above is inserted + if '/'+path.strip('/') != "/": + return('/'+path.strip('/')) + else: + return(None) + + def getSSHLogin(self,store='SRC',tag='DEFAULT'): + if self.getMountPath(store=store,tag=tag,shlogin=True) is None: + return(None) + else: + uh,p = self.getMountPath(store=store,tag=tag,shlogin=True) + return('ssh -p %s %s ' % (p,uh)) + + + def getStoreName(self,store='SRC',tag='DEFAULT'): + if store == 'SRC': + try: + path = self.config.get(tag,'srcstore').strip('/') + except: + path = self.config.get('DEFAULT','srcstore').strip('/') + elif store == 'SNP': + try: + path = self.__trnName(self.config.get(tag,'snpstore').strip('/')) + except: + path = self.__trnName(self.config.get('DEFAULT','snpstore').strip('/')) + elif store == 'BKP': + try: + path = self.__trnName(self.config.get(tag,'bkpstore').strip('/')) + except: + path = self.__trnName(self.config.get('DEFAULT','bkpstore').strip('/')) + if '/'+path.strip('/') != "/": + return(path.strip('/')) + else: + return('') + + def getStorePath(self,store='SRC',tag='DEFAULT',original=False): + sn = '/'+self.getStoreName(store=store,tag=tag) if len(self.getStoreName(store=store,tag=tag)) > 0 else '' + return(self.getMountPath(store=store,tag=tag,original=original) + sn) + + def cmdsh(self,tag='DEFAULT',store='SRC',cmd=''): + if self.getssh(tag,store) == None: + return('',subprocess.check_output(cmd, shell=True).decode(),'') + else: + out = '' + conn = self.getssh(tag,store) + connect(conn) + return conn['conn'].exec_command(cmd) + + def remotecommand(self,tag='DEFAULT',store='SRC',cmd='',stderr=None): + if self.getssh(tag,store) == None: + #print("noconn") + try: + ret = subprocess.run(cmd,stderr=stderr,stdout=subprocess.PIPE) + if ret.returncode > 0: + pass + except subprocess.CalledProcessError as e: + raise + return ret.stdout.decode("utf-8").rstrip('/n') + + else: + #print("conn",self.ssh[tag][store]['host']) + out = '' + conn = self.getssh(tag,store) + if connect(conn): + stdin, stdout, stderr = conn['conn'].exec_command(' '.join(cmd)) + if not stdout: + #print("Xr") + out = stdout.readlines() + err = stderr.readlines() + return(''.join(out) if len(err) == 0 else False) + else: + #print("Yr",stdout.read().decode("utf-8")) + return(stdout.read().decode("utf-8")) + else: + print("Host not reachable (remcomd): %s" % (conn['host'])) + return('') + + + def getDevice(self,store='SRC',tag='DEFAULT'): + mp = self.getMountPath(store=store,tag=tag,original=False) + conn = self.getssh(tag,store) + connect(conn) + mi = MountInfo(conn=conn) + amount=mi.fstype(mp) + if mi.fstype(mp) == 'autofs': + try: + Myos().stat(self.getStorePath(store=store,tag=tag),conn=conn) + except: + Myos().stat(os.path.dirname(self.getStorePath(store=store,tag=tag)),conn=conn) + mi = MountInfo(conn=self.getssh(tag,store)) + return(mi.device(mp) if mi.fstype(mp) != 'autofs' else None) + + def getUUID(self,store='SRC',tag='DEFAULT'): + try: + device = self.getDevice(store=store,tag=tag) + except FileNotFoundError: + #print("UID_NOENT") + #raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), store + " " + tag) + raise + except: + return None + + #print("DEVICE",device,store,tag) + if device == None: return None + cmd = ['/sbin/blkid', device.rstrip("\n"), '-o', 'value', '-s', 'UUID'] + uuid = self.remotecommand(tag,store,cmd) + #print("UUID",uuid,store,tag,' '.join(cmd)) + return uuid.rstrip('\n') if uuid.rstrip('\n') != '' else None + + + def setBKPPath(self,mount): + #self._read() + self.config['DEFAULT']['BKPMNT'] = mount + + def setBKPStore(self,store): + #self._read() + self.config['DEFAULT']['bkpstore'] = store + + def setSNPPath(self,mount): + #self._read() + self.config['DEFAULT']['SNPMNT'] = mount + + def setSNPStore(self,store): + #self._read() + self.config['DEFAULT']['snpstore'] = store + + def getInterval(self,intv='misc'): + #self._read() + try: + return(self.config.get(intv,'interval')) + except: + return(self.config.get('DEFAULT','interval')) + + def getTransfer(self,intv='misc'): + #self._read() + try: + return(s2bool(self.config.get(intv,'transfer'))) + except: + return(s2bool(self.config.get('DEFAULT','transfer'))) + + def getSymLink(self,intv='misc'): + #self._read() + try: + return(self.config.get(intv,'symlink')) + except: + return(self.config.get('DEFAULT','symlink')) + + def getIsDefault(self,intv='misc'): + #self._read() + try: + self.config.get(intv,'interval') + return(intv) + except: + return('default') + + def getVolumes(self,tag='default'): + #self._read() + VOLSTRANS = [] + try: + VOLS = self.config.get(tag,'volumes') + except: + VOLS = self.config.get('DEFAULT','volumes') + for vol in VOLS.split(','): + VOLSTRANS.append(self.__trnName(vol)) + return(VOLSTRANS) + + def ListIntVolumes(self): + #self._read() + VOLSTRANS = [] + for intv in self.ListIntervals(): + try: + VOLS = self.config.get(intv,'volumes') + except: + VOLS = self.config.get('DEFAULT','volumes') + VOLS = VOLS.split(',') + for i, item in enumerate(VOLS): + VOLS[i] = self.__trnName(item) + VOLSTRANS.append('\t'+intv+': '+' '.join(VOLS)) + return(VOLSTRANS) + + def getIgnores(self,intv='misc'): + try: + r = self.config.get(intv,'ignore') + except: + try: + r = self.config.get('DEFAULT','ignore') + except: + r=None + return(None if r == '' or r == None else r) + + def getNotification(self,intv='misc'): + #print('GN',intv) + try: + r = self.config.get(intv,'notification') + except: + try: + r = self.config.get('DEFAULT','notification') + except: + r=None + return(None if r == '' or r == None else r) + + def getUrgency(self,intv='misc'): + #print('GU',intv) + try: + r = self.config.get(intv,'notification_urgency') + except: + try: + r = self.config.get('DEFAULT','notification_urgency') + except: + r=None + return(None if r == '' or r == None else r) + + def __trnName(self,short): + ret = list() + for sh in short.split(','): + if sh == "$S": ret.append(self.syssubvol) + elif sh == "$h": ret.append(self.hostname) + else: ret.append(sh) + return(','.join(ret)) + diff --git a/files/usr/lib/python3/dist-packages/mkbackup/mkbackup_emitter.py b/files/usr/lib/python3/dist-packages/mkbackup/mkbackup_emitter.py new file mode 100644 index 0000000..8298412 --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/mkbackup_emitter.py @@ -0,0 +1,49 @@ +"""Emmitter functionality.""" +import dbus +import dbus.service +import dbus.glib + + +class Emitter(dbus.service.Object): + """Emitter DBUS service object.""" + + def __init__(self, conn=None, object_path=None, bus_name=None): + """Initialize the emitter DBUS service object.""" + dbus.service.Object.__init__(self, conn=conn, object_path=object_path) + + @dbus.service.signal(dbus_interface='at.xundeenergie.mkbackup.Status') + def update(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a update signal') + + @dbus.service.signal(dbus_interface='at.xundeenergie.mkbackup.Status') + def start(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a start signal') + + @dbus.service.signal(dbus_interface='at.xundeenergie.mkbackup.Status') + def finished(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a finished signal') + + @dbus.service.signal(dbus_interface='at.xundeenergie.mkbackup.Status') + def reset(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a reset signal') + +""" Example to use +progress = Emitter(dbus.SystemBus(), + '/at/xundeenergie/mkbackup/Status') + +progress.start( + {'intv': 'hourly'}) + +progress.update( + {'intv': 'hourly', 'progr': 5}) + +progress.finished( + {'intv': 'hourly'}) + +progress.reset( + {'intv': 'hourly'}) +""" diff --git a/files/usr/lib/python3/dist-packages/mkbackup/service b/files/usr/lib/python3/dist-packages/mkbackup/service new file mode 100755 index 0000000..427bd9b --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/service @@ -0,0 +1,41 @@ +#!/usr/bin/python3 -d +#!/usr/bin/env python3 + +import dbus, dbus.service, dbus.exceptions +import sys + +from dbus.mainloop.glib import DBusGMainLoop +from gi.repository import GLib + +# Initialize a main loop +DBusGMainLoop(set_as_default=True) +loop = GLib.MainLoop() + +# Declare a name where our service can be reached +try: + sysbus_name = dbus.service.BusName("at.xundeenergie", + bus=dbus.SystemBus(), + do_not_queue=True) +except dbus.exceptions.NameExistsException: + print("service is already running") + sys.exit(1) + +# Run the loop +try: + # Create our initial objects + from services.mkbackup import MkBackup + #from services.notifications import AllStati + #from services.notifications import Properties + #from services.mkbackup import Properties + MkBackup(sysbus_name, "/at/xundeenergie/mkbackup/Intervals") + #AllStati(sysbus_name, "/at/xundeenergie/mkbackup") + #Properties(sysbus_name, "/at/xundeenergie/mkbackup") + #Properties(sysbus_name, dbus_interface="/at/xundeenergie/mkbackup") + + loop.run() +except KeyboardInterrupt: + print("keyboard interrupt received") +except Exception as e: + print("Unexpected exception occurred: '{}'".format(str(e))) +finally: + loop.quit() diff --git a/files/usr/lib/python3/dist-packages/mkbackup/services/__init__.py b/files/usr/lib/python3/dist-packages/mkbackup/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/__init__.cpython-36.pyc b/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000..a9011a3 Binary files /dev/null and b/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/__init__.cpython-36.pyc differ diff --git a/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/mkbackup.cpython-36.pyc b/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/mkbackup.cpython-36.pyc new file mode 100644 index 0000000..602b552 Binary files /dev/null and b/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/mkbackup.cpython-36.pyc differ diff --git a/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/notifications.cpython-36.pyc b/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/notifications.cpython-36.pyc new file mode 100644 index 0000000..0cb6ce5 Binary files /dev/null and b/files/usr/lib/python3/dist-packages/mkbackup/services/__pycache__/notifications.cpython-36.pyc differ diff --git a/files/usr/lib/python3/dist-packages/mkbackup/services/mkbackup.py b/files/usr/lib/python3/dist-packages/mkbackup/services/mkbackup.py new file mode 100644 index 0000000..edfad8b --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/services/mkbackup.py @@ -0,0 +1,117 @@ +import dbus +import dbus.service +import random +import time +import os + +from mkbackup_btrfs_config import Config, MountInfo, connect, Myos, __version__ + +config = Config() + +class MkBackup: + def __init__(self, bus_name, base_path): +# self._bus = dbus.SystemBus() +# self.notification = Notification() + Intervals(bus_name, base_path) + + for intv in config.ListIntervals(): + Properties(bus_name, os.path.join(base_path, intv), intv) + +class Intervals(dbus.service.Object): + def __init__(self, bus_name, bus_path): + super().__init__(bus_name, bus_path) + + @dbus.service.method(dbus_interface='at.xundeenergie.mkbackup.Intervals', + in_signature='', out_signature='v') + def Names(self): + return config.ListIntervals() + +class Properties(dbus.service.Object): + def __init__(self, bus_name, bus_path, interval): + super().__init__(bus_name, bus_path) + self.interface = "at.xundeenergie.mkbackup.Status" + self.interval = interval + self.STATI = ['reset', 'stop', 'running', 'finished'] + self.properties = dict() + self.properties[self.interface] = dict() + self.properties[self.interface]['progress'] = 0 # 0-100 + self.properties[self.interface]['status'] = 'stop' # stop, running, finished, reset + self.properties[self.interface]['transfer'] = config.getTransfer(interval) + self.properties[self.interface]['lastrun'] = 0 # datetime + self.properties[self.interface]['finished'] = True # Boolean + self.properties[self.interface]['name'] = interval # Boolean + self.properties['function'] = dict() + self.properties['function']['progress'] = self.update_progress + self.properties['function']['status'] = self.update_status +# self.properties['function']['lastrun'] = self.update_lastrun + from dbus import Interface + + def update_progress(self, interface, incr): + print("Update progress: %i / %i, %s" % (float(incr), + self.properties[interface]['progress'],self.properties[interface]['status'])) + if self.properties[interface]['status'] == 'running': + if 0 < self.properties[interface]['progress'] + float(incr) < 100: + #self.properties[interface]['progress'] = self.properties[interface]['progress'] + float(incr) + self.properties[interface]['progress'] += float(incr) + elif self.properties[interface]['progress'] + float(incr) >= 100: + self.properties[interface]['progress'] = 99 + else: + print('B', incr, type(incr)) + return self.properties[interface]['progress'] + + def update_status(self, interface, status): + if status in self.STATI: + print("Update status: %s" % status) + print("Status: ", self.properties[interface]['status']) + if status == 'finished': + self.properties[interface]['status'] = status + self.properties[interface]['progress'] = 100 + elif status == 'stop': + self.properties[interface]['status'] = status + self.properties[interface]['progres'] = 0 + elif status == 'reset': + self.properties[interface]['status'] = 'running' + self.properties[interface]['progress'] = 0 + print("status: ", self.properties[interface]['status']) + return self.properties[interface]['status'] + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='ss', out_signature='v') + def Get(self, interface_name, property_name): + return self.GetAll(interface_name)[property_name] + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='s', out_signature='a{sv}') + def GetAll(self, interface_name): + if interface_name == self.interface: + return self.properties[interface_name] + else: + raise dbus.exceptions.DBusException( + 'at.xundeenergie.mkbackup.UnknownInterface', + 'The Foo object does not implement the %s interface' + % interface_name) + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='ssv') + def Set(self, interface_name, property_name, new_value): + # validate the property name and value, update internal state… + """https://recalll.co/ask/v/topic/D-Bus-D-Feet-Send-Dictionary-of-String%2CVariants-in-Python-Syntax/5565e1372bd273d7108b7b82 + __import__('gi.repository.GLib', globals(), locals(), ['Variant']).Variant("s", "value")""" + if interface_name in self.properties: + if property_name in self.properties[interface_name]: + func = self.properties['function'].get(property_name) + new_value = func(interface_name, new_value) + #self.properties[str(interface_name)][str(property_name)] = new_value + self.PropertiesChanged(interface_name, + { property_name: new_value, 'interval': self.interval}, []) + else: + raise dbus.exceptions.DBusException( + 'at.xundeenergie.mkbackup.UnknownInterface', + 'The Foo object does not implement the %s interface' + % interface_name) + + @dbus.service.signal(dbus.PROPERTIES_IFACE, + signature='sa{sv}as') + def PropertiesChanged(self, interface_name, changed_properties, + invalidated_properties): + pass diff --git a/files/usr/lib/python3/dist-packages/mkbackup/services/notifications.py b/files/usr/lib/python3/dist-packages/mkbackup/services/notifications.py new file mode 100644 index 0000000..dff93df --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/services/notifications.py @@ -0,0 +1,242 @@ +import dbus +import dbus.service +import random +import time +import os + +from gi.repository import GLib + +from mkbackup_btrfs_config import Config, MountInfo, connect, Myos, __version__ +config = Config() + +from system_notification_emitter import Emitter +class Notification(Emitter): + def __init__(self): + super().__init__(conn=dbus.SystemBus(), object_path='/at/xundeenergie/notifications/advanced/Notification') + +class Intervals: + def __init__(self, bus_name, base_path="/at/xundeenergie/mkbackup"): + #super().__init__(bus_name, "/at/xundeenergie/mkbackup") + self.base_path = base_path + self._bus = dbus.SystemBus() +# self.stati = dict() +# self.stati[None] = None + self.notification = Notification() + + for intv in config.ListIntervals(): +# self.stati[intv] = dict() +# self.stati[intv]['progress'] = 0 +# self.stati[intv]['trans'] = config.getTransfer(intv) +# self.stati[intv]['lastrun'] = 0 +# self.stati[intv]['finished'] = True + #self._set_listeners(os.path.join(base_path, intv)) +# self.stati[intv]['methods'] = Properties(bus_name, +# os.path.join(base_path, intv)) + Properties(bus_name, os.path.join(base_path, intv), intv) + + def _set_listeners(self,dbus_path): + """ + dbus-send --system "/at/xundeenergie/mkbackup" + --dest="at.xundeenergie.mkbackup" + "at.xundeenergie.mkbackup.Status.progress" string:hourly "int16:10" + """ + update = self._bus.add_signal_receiver( + path=dbus_path, + handler_function=self._progress, + dbus_interface="at.xundeenergie.mkbackup.Status", + signal_name='update') + + finished = self._bus.add_signal_receiver( + path=dbus_path, + handler_function=self._finished, + dbus_interface="at.xundeenergie.mkbackup.Status", + signal_name='finished') + + start = self._bus.add_signal_receiver( + path=dbus_path, + handler_function=self._start, + dbus_interface="at.xundeenergie.mkbackup.Status", + signal_name='start') + + reset = self._bus.add_signal_receiver( + path=dbus_path, + handler_function=self._reset, + dbus_interface="at.xundeenergie.mkbackup.Status", + signal_name='reset') + + def _remove_listeners(self,intv): + update = self._bus.remove_signal_receiver( + path=dbus_path, + dbus_interface="at.xundeenergie.mkbackup.Status", + signal_name='update') + + finished = self._bus.remove_signal_receiver( + path=dbus_path, + dbus_interface="at.xundeenergie.mkbackup.Status", + signal_name='finished') + + start = self._bus.remove_signal_receiver( + path=dbus_path, + dbus_interface="at.xundeenergie.mkbackup.Status", + signal_name='start') + + reset = self._bus.remove_signal_receiver( + path=dbus_path, + dbus_interface="at.xundeenergie.mkbackup.Status", + signal_name='reset') + + try: + del self.state[intv] + except KeyError as ex: + print("No such key: '%s'" % ex.message) + + + def _start(self, intv): + self.stati[intv]['progress'] = 0 + if self.stati[intv]['finished']: + print("Reset %s first" % (intv)) + return + print("%s start" % (intv)) + + def _progress(self, intv, progr): + if progr >= 0 and not self.stati[intv]['finished']: + if 0 < self.stati[intv]['progress'] + progr <= 99: + self.stati[intv]['progress'] += progr + print("%s run progress: %i/100%%" % (str(intv), + int(self.stati[intv]['progress']))) + else: + self.stati[intv]['progress'] = 99 + self.sig_update(intv, self.stati[intv]['progress']) + print("Progress: %s" % (self.stati[intv]['progress'])) + + def _finished(self, intv): + self.stati[intv]['finished'] = True + self.stati[intv]['progress'] = 100 + self._send_notification(body="%s finished" % (intv)) + self.sig_update(intv, self.stati[intv]['progress']) + print("%s finished" % (intv)) + + def _reset(self, intv): + self.stati[intv]['finished'] = False + self.stati[intv]['progress'] = 0 + print("%s reset" % (intv)) + + def _send_notification(self, header="backup", body="Unconfigured Message"): + msg = dict() + msg['sender'] = "mkbackup" + msg['header'] = header + msg['body'] = body + self.notification.normal(msg) + + +class AllStati(dbus.service.Object): + def __init__(self, bus_name, base_path="/at/xundeenergie/mkbackup"): + super().__init__(bus_name, base_path) + + @dbus.service.method("at.xundeenergie.mkbackup", + in_signature='', out_signature='v') + def Intervals(self): + return config.ListIntervals() + +MY_INTERFACE = 'at.xundeenergie.mkbackup.Properties' + +class Status(dbus.service.Object): + def __init__(self, bus_name, base_path, intv, stati): + super().__init__(bus_name, base_path+'/'+intv) + self.interface_name = base_path+'/'+intv + self.property_name = intv + self.status = stati + + @dbus.service.method("at.xundeenergie.mkbackup.Status", + in_signature='', out_signature='v') + def Progress(self): + return self.status['progress'] + + @dbus.service.method("at.xundeenergie.mkbackup.Status", + in_signature='', out_signature='a{sv}') + def Props(self): + return { + 'progress': self.status['progress'], + 'transfer': self.status['trans'], + 'lastrun' : self.status['lastrun'], + 'finished': self.status['finished'] + } + + @dbus.service.signal("at.xundeenergie.mkbackup.Status", signature='si') + def sig_update(self, intv, progr): + pass + + @dbus.service.signal("at.xundeenergie.mkbackup.Status", signature='si') + def sig_finished(self, intv, progr): + pass + +class Properties(dbus.service.Object): + def __init__(self, bus_name, bus_path, interval): + super().__init__(bus_name, bus_path) + self.interface_name = dbus.PROPERTIES_IFACE + self.interface = "at.xundeenergie.mkbackup.Status" + self.interval = interval + self.properties = dict() + self.properties[self.interface] = dict() + self.properties[self.interface]['progress'] = 0 + self.properties[self.interface]['transfer'] = config.getTransfer(interval) + self.properties[self.interface]['lastrun'] = 0 + self.properties[self.interface]['finished'] = True + from dbus import Interface + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='ss', out_signature='v') + def Get(self, interface_name, property_name): + return self.GetAll(interface_name)[property_name] + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='s', out_signature='a{sv}') + def GetAll(self, interface_name): + print('I', interface_name, 'sI', self.interface, 'P', self.properties, + 'PP',self.properties[interface_name]) + if interface_name == self.interface: + return self.properties[interface_name] + else: + raise dbus.exceptions.DBusException( + 'com.example.UnknownInterface', + 'The Foo object does not implement the %s interface' + % interface_name) + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature='ssv') + def Set(self, interface_name, property_name, new_value): + # validate the property name and value, update internal state… + """https://recalll.co/ask/v/topic/D-Bus-D-Feet-Send-Dictionary-of-String%2CVariants-in-Python-Syntax/5565e1372bd273d7108b7b82 + __import__('gi.repository.GLib', globals(), locals(), ['Variant']).Variant("s", "value")""" + if interface_name == self.interface: + self.properties[str(interface_name)][str(property_name)] = new_value + self.PropertiesChanged(str(interface_name), + { str(property_name): new_value }, []) + + @dbus.service.signal(dbus.PROPERTIES_IFACE, + signature='sa{sv}as') + def PropertiesChanged(self, interface_name, changed_properties, + invalidated_properties): + pass +#class Notifications(dbus.service.Object): +# def __init__(self, bus_name): +# super().__init__(bus_name, "/at/xundeenergie/mkbackup/Status") +# +# random.seed() +# +# @dbus.service.method("at.xundeenergie.mkbackup.Status", +# in_signature='i', out_signature='s') +# def quick(self, bits=8): +# return str(random.getrandbits(bits)) +# +# @dbus.service.method("at.xundeenergie.mkbackup.Status", +# in_signature='i', out_signature='s') +# def slow(self, bits=8): +# thread = SlowThread(bits, self.slow_result) +# return thread.thread_id +# +# @dbus.service.signal("at.xundeenergie.mkbackup.Status", signature='ss') +# def slow_result(self, thread_id, result): +# pass +# +# diff --git a/files/usr/lib/python3/dist-packages/mkbackup/services/random_data.py b/files/usr/lib/python3/dist-packages/mkbackup/services/random_data.py new file mode 100644 index 0000000..cf22549 --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/services/random_data.py @@ -0,0 +1,47 @@ +import dbus.service +import random +import time + +import threading +class SlowThread(object): + def __init__(self, bits, callback): + self._callback = callback + self.result = '' + + self.thread = threading.Thread(target=self.work, args=(bits,)) + self.thread.start() + self.thread_id = str(self.thread.ident) + + def work(self, bits): + num = '' + + while True: + num += str(random.randint(0, 1)) + bits -= 1 + time.sleep(1) + + if bits <= 0: + break + + self._callback(self.thread_id, str(int(num, 2))) + +class RandomData(dbus.service.Object): + def __init__(self, bus_name): + super().__init__(bus_name, "/com/larry_price/test/RandomData") + + random.seed() + + @dbus.service.method("com.larry_price.test.RandomData", + in_signature='i', out_signature='s') + def quick(self, bits=8): + return str(random.getrandbits(bits)) + + @dbus.service.method("com.larry_price.test.RandomData", + in_signature='i', out_signature='s') + def slow(self, bits=8): + thread = SlowThread(bits, self.slow_result) + return thread.thread_id + + @dbus.service.signal("com.larry_price.test.RandomData", signature='ss') + def slow_result(self, thread_id, result): + pass diff --git a/files/usr/lib/python3/dist-packages/mkbackup/system_notification_emitter.py b/files/usr/lib/python3/dist-packages/mkbackup/system_notification_emitter.py new file mode 100644 index 0000000..8e4b870 --- /dev/null +++ b/files/usr/lib/python3/dist-packages/mkbackup/system_notification_emitter.py @@ -0,0 +1,38 @@ +"""Emmitter functionality.""" +import dbus +import dbus.service +import dbus.glib + + +class Emitter(dbus.service.Object): + """Emitter DBUS service object.""" + + def __init__(self, conn=None, object_path=None, bus_name=None): + """Initialize the emitter DBUS service object.""" + dbus.service.Object.__init__(self, conn=conn, object_path=object_path) + + @dbus.service.signal(dbus_interface='at.xundeenergie.Notification') + def low(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a low test signal') + + @dbus.service.signal(dbus_interface='at.xundeenergie.Notification') + def normal(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a normal test signal') + + @dbus.service.signal(dbus_interface='at.xundeenergie.Notification') + def critical(self,*args,**kwargs): + """Emmit a test signal.""" + print('Emitted a critical test signal') + +""" Example to use +Simple_Notification = Emitter(dbus.SystemBus(), + '/at/xundeenergie/notifications/simple/Notification') +Advanced_Notification = Emitter(dbus.SystemBus(), + '/at/xundeenergie/notifications/advanced/Notification') + +#Simple_Notification.low('M') +Advanced_Notification.normal( + {'sender': 'emitter1.py', 'header': 'Testmessage', 'body': 'Test Body'}) +""" diff --git a/files/usr/lib/systemd/scripts/btrfs-action.sh b/files/usr/lib/systemd/scripts/btrfs-action.sh new file mode 100755 index 0000000..2472486 --- /dev/null +++ b/files/usr/lib/systemd/scripts/btrfs-action.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +DEVICE="$1"; shift +ACTION="$1"; shift +MAINPID="$1" +BTRFS=/bin/btrfs + +[ x"$MAINPID" = "x" ] && exit 0 +/bin/ps h -o command -p "$MAINPID" && $BTRFS $ACTION cancel "$DEVICE" || exit 0 diff --git a/files/usr/lib/systemd/scripts/mksnapshot-create-volume.sh b/files/usr/lib/systemd/scripts/mksnapshot-create-volume.sh new file mode 100755 index 0000000..5250d03 --- /dev/null +++ b/files/usr/lib/systemd/scripts/mksnapshot-create-volume.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +# Create udev-rule and mount-entry for new backup-volume + +ACTION=$1 +case $2 in + -u) + UUID=$3 + DEV=$(readlink -f /dev/disk/by-uuid/$UUID) + PARTUUID="$(blkid /dev/disk/by-uuid/$UUID -o value -s PARTUUID)" + ;; + u-*) + UUID=${2#u-} + DEV=$(readlink -f /dev/disk/by-uuid/$UUID) + PARTUUID="$(blkid /dev/disk/by-uuid/$UUID -o value -s PARTUUID)" + ;; + -p) + PARTUUID=$3 + DEV=$(readlink -f /dev/disk/by-partuuid/$PARTUUID) + UUID="$(blkid /dev/disk/by-partuuid/$PARTUUID -o value -s UUID)" + ;; + p-*) + PARTUUID=${2#p-} + DEV=$(readlink -f /dev/disk/by-partuuid/$PARTUUID) + UUID="$(blkid /dev/disk/by-partuuid/$PARTUUID -o value -s UUID)" + ;; + d-*) + DEV=${2#d-} + UUID="$(blkid $DEV -o value -s UUID)" + PARTUUID="$(blkid $DEV -o value -s PARTUUID)" + ;; + *) + DEV="$(/bin/systemd-escape -p -u $2)" + UUID="$(blkid $DEV -o value -s UUID)" + #PARTUUID="$(blkid $DEV -o value -s PARTUUID)" + PRE="d-" + ;; +esac + +#DESTUDEV="/tmp/" +#DESTSYSTEMD="/tmp/" +DESTUDEV="/etc/udev/rules.d/" +DESTSYSTEMD="/etc/systemd/system/" + +SYSTEMCTL="/bin/systemctl" + +echo "$ACTION ${DEV} | ${UUID} | ${PARTUUID}" + +sleep 1 + +if [ "$DEV"x = "x" ] +then + TYPE="btrfs" +else + TYPE="$(blkid $DEV -o value -s TYPE)" + echo "T $TYPE | $DEV" +fi + +if [ "$PARTUUID"x = "x" ]; then + if [ "$UUID"x = "x" ]; then + echo "$PARTUUID | $UUID | $DEV is no valid device" + exit 3 + else + DUUID="$UUID" #DUUID is uuid which is taken to use + SUUID="ID_FS_UUID" #SUUID is the string for the udev-rule it's UUID or PARTUUID + ID="uuid" #ID is also for the udev-rule. To look in /dev/disk/by-uuid or /dev/disk/by-partuuid + PRE="u-" + fi +else + DUUID="$PARTUUID" + SUUID="ID_PART_ENTRY_UUID" + ID="partuuid" + PRE="p-" +fi + +#echo "$DUUID | $SUUID | $ID | $PRE" +# Start by udev +start () { + +mkdir -p "${DESTSYSTEMD}var-cache-backup.mount.d/" + +cat < "${DESTSYSTEMD}var-cache-backup.mount.d/source.conf" +[Mount] +What=/dev/disk/by-${ID}/${DUUID} +EOF + +$SYSTEMCTL daemon-reload + +} + +# Create udev-Rule for new external drive +register () { +cat < "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules" +ACTION=="add", KERNEL=="sd*", SUBSYSTEMS=="usb", ENV{${SUUID}}=="$DUUID", SYMLINK+="disk/mars", TAG+="systemd", ENV{SYSTEMD_WANTS}+="mkbackup-external@${PRE}${DUUID}.service", ENV{SYSTEMD_WANTS}+="mkbackup@BKP.target", ENV{SYSTEMD_WANTS}+="smartctl-fast@$(/bin/systemd-escape -p /dev/disk/by-${ID}/${DUUID}).service" + +ACTION=="remove", KERNEL=="sd*", SUBSYSTEMS=="usb", ENV{${SUUID}}="$DUUID", \ +RUN+="${SYSTEMCTL} --no-block stop mkbackup@BKP.target" + +EOF + +} + + +# delete udev-rule, if external drive is not longer in use for backups. +unregister () { +[ -e "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules" ] && rm "${DESTUDEV}99-ext-bkp-volume-${PRE}${DUUID}.rules" +} + +case $TYPE in + btrfs) + ;; + *) + echo "$DEV isn't a btrfs-filesystem. Exiting"; exit 1;; +esac + +case $ACTION in + register) + #setup udev-rule for device + register + ;; + unregister) + #delete udev-rule for device + unregister ;; + start) + #activate device + start;; + stop) + #deactivate device + stop ;; + *) + echo "$ACTION not recognized"; + exit 2;; +esac + +$SYSTEMCTL daemon-reload +#/bin/systemctl +exit 0 diff --git a/files/usr/lib/systemd/scripts/systemd-btrfs-email b/files/usr/lib/systemd/scripts/systemd-btrfs-email new file mode 100755 index 0000000..9eff14f --- /dev/null +++ b/files/usr/lib/systemd/scripts/systemd-btrfs-email @@ -0,0 +1,12 @@ +#!/bin/bash + +/usr/sbin/sendmail -t < +Subject: $2 +Content-Transfer-Encoding: 8bit +Content-Type: text/plain; charset=UTF-8 + +$(btrfs scrub status "$3") + +ERRMAIL diff --git a/files/usr/lib/systemd/user/mkbackup-userdir.path b/files/usr/lib/systemd/user/mkbackup-userdir.path new file mode 100644 index 0000000..1efb362 --- /dev/null +++ b/files/usr/lib/systemd/user/mkbackup-userdir.path @@ -0,0 +1,11 @@ +[Unit] +#Description=Starts mounting backups to %h/backup, if this directory exists +#BindsTo=mkbackup-userdir.path +Conflicts=shutdown.target sleep.target suspend.target + +[Path] +PathExists=%h/backup +Unit=mkbackup-userdir.service + +[Install] +WantedBy=paths.target diff --git a/files/usr/lib/systemd/user/mkbackup-userdir.service b/files/usr/lib/systemd/user/mkbackup-userdir.service new file mode 100644 index 0000000..5ae0b85 --- /dev/null +++ b/files/usr/lib/systemd/user/mkbackup-userdir.service @@ -0,0 +1,10 @@ +[Unit] +Description=Show all userspezific backups in %h/backup +BindsTo=mkbackup-userdir.path +Conflicts=shutdown.target reboot.target umount.target +Before=shutdown.target reboot.target sleep.target umount.target + +[Service] +ExecStart=/usr/bin/MksnapshotFS.py -f %h/backup -o ro,allow_root +ExecStop=-/bin/fusermount -u %h/backup +Restart=on-success diff --git a/files/usr/lib/systemd/user/paths.target.wants/mkbackup-userdir.path b/files/usr/lib/systemd/user/paths.target.wants/mkbackup-userdir.path new file mode 120000 index 0000000..dc66363 --- /dev/null +++ b/files/usr/lib/systemd/user/paths.target.wants/mkbackup-userdir.path @@ -0,0 +1 @@ +../mkbackup-userdir.path \ No newline at end of file diff --git a/files/usr/local/bin/mkbackup b/files/usr/local/bin/mkbackup new file mode 120000 index 0000000..398b331 --- /dev/null +++ b/files/usr/local/bin/mkbackup @@ -0,0 +1 @@ +mkbackup-btrfs \ No newline at end of file diff --git a/files/usr/local/bin/mkbackup-btrfs b/files/usr/local/bin/mkbackup-btrfs new file mode 100755 index 0000000..8905cc3 --- /dev/null +++ b/files/usr/local/bin/mkbackup-btrfs @@ -0,0 +1,2156 @@ +#!/usr/bin/python3 -u +#!/usr/bin/pkexec /usr/bin/python3 + +import argparse +import re +import datetime +import subprocess +import os +import sys +import errno +import glob +import progressbar as pb +import shutil +import psutil +# sqlite3 is used for writing database about last runs - it is read by list and from gnome-extension (planned) +import sqlite3 + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +# --- Logger --- +import logging + + +try: + from anytree import Node, RenderTree + TREE=True +except: + TREE=False + +import dbus +import dbus.service +import dbus.glib +from mkbackup.system_notification_emitter import Emitter +from mkbackup.mkbackup_emitter import Emitter as EM + +#from mksnapshotconfig import Config +from mkbackup.mkbackup_btrfs_config import Config, MountInfo, connect, Myos, __version__ +from mkbackup.mkbackup_btrfs_config import __version__ as confversion + +__author__ = "Jakobus Schürz " +__version__ = "1.01.0" + +class Notification(Emitter): + def __init__(self): + super().__init__(conn=dbus.SystemBus(), object_path='/at/xundeenergie/notifications/advanced/Notification') + +class EmDBUS(EM): + """ Example to use + progress = Emitter(dbus.SystemBus(), + '/at/xundeenergie/mkbackup/Status') + + progress.start( + {'intv': 'hourly'}) + + progress.update( + {'intv': 'hourly', 'progr': 5}) + + progress.finished( + {'intv': 'hourly'}) + + progress.reset( + {'intv': 'hourly'}) + """ + def __init__(self): + super().__init__(conn=dbus.SystemBus(), object_path='/at/xundeenergie/mkbackup/Status') + +#define progress timer class +class progress_timer: + + def __init__(self, n_iter, description="Something"): + self.n_iter = n_iter + self.iter = 0 + self.description = description + ': ' + self.timer = None + self.initialize() + + def initialize(self): + #initialize timer + widgets = [self.description, pb.Percentage(), ' ', + pb.Bar(marker=pb.RotatingMarker()), ' ', pb.ETA()] + self.timer = pb.ProgressBar(widgets=widgets, maxval=self.n_iter).start() + + def update(self, q=1): + #update timer + self.timer.update(self.iter) + self.iter += q + + def finish(self): + #end timer + self.timer.finish() + +# RuntimeError definitions: +# RuntimeError(msg, code) +# code: +# 0 - continue programm +# 1 - raise error + +if hasattr(os, 'sync'): + sync = os.sync +else: + import ctypes + libc = ctypes.CDLL('libc.so.6') + def sync(): + libc.sync() + +def DEBUG(*msg,level=0,verbose=0): + if verbose < level : + return + else: + #print(' '.join(msg)) + for i in msg: print(i) + return + +def isstring(s): + # if we use Python 3 + if (sys.version_info[0] == 3): + return isinstance(s, str) + # we use Python 2 + return isinstance(s, basestring) + +def check_lockfile(args,lf): + # Returns True, when lockfile is in use, and False if lockfile is unused + # Returns None, when lockfile is not existing + for st in args.store: + if Myos().path_isfile(lf,args.config.getssh(args.tag,st)): + file = open(lf, 'r') + pid = file.readline() + file.close() + #if len(pid) > 0 and Myos().path_isfile('/proc/'+pid+'/cmdline',args.config.ssh[args.tag][st]): + if len(pid) > 0 and os.path.isfile('/proc/'+pid+'/cmdline'): + logger.warn('lockfile %s in use with process %s' % (lf,pid)) + #DEBUG('lockfile %s in use with process %s' % (lf,pid),level=3,verbose=args.verbose) + return(True) + else: + logger.warn('lockfile %s unused' % (lf)) + #DEBUG('lockfile %s unused' % (lf),level=3,verbose=args.verbose) + return(False) + else: + logger.warn('lockfile %s not existing' % (lf)) + #DEBUG('lockfile %s not existing' % (lf),level=3,verbose=args.verbose) + return(None) + +class Stack: + def __init__(self): + self.item = [] + + def push(self,item): + self.item.append(item) + + def pop(self): + return self.item.pop() + + def is_empty(self): + return(self.item == []) + + def len(self): + return(len(self.item)) + + def all(self): + return(self.item) + + +class Error(Exception): + pass + +class NoSubvolumeError(Error): + def __init__(self): + print("ERROR - Snapshot not found" ) + pass + +class ScanFsError(Error): + def __init__(self,vol='unknown'): + print("ERROR - scanfs. Volume not found or no Permissions: %s" % (vol)) + pass + pass + +class NoBtrfsVolumeError(Error): + def __init__(self,vol='unknown'): + pass + pass + +class TransferError(Error): + pass + +class CreateError(Error): + pass + +class SetpropError(Error): + pass + +class DeleteError(Error): + pass + +class SystemVolumeError(Error): + def __init__(self,action='undefined'): + print("Action %s on Systemvolume not allowed" % (action)) + +class BtrfsListing: + what = "BtrfsListing of all subvolumes in store" + CreatedSubvolumes = Stack() + DeletedSubvolumes = Stack() + TransferedSubvolumes = Stack() + + + + def __init__(self,args,store='SRC',single=False): + #BtrfsListing.Lists.push(store) + self.args = args + self.store = store + self.single = single + self.verbose = args.verbose + self.StoreList = store # List is from which store + self.MountPath = args.config.getMountPath(self.store,self.args.tag,original=False) # path from / to the mountpoint of store + self.StoreName = args.config.getStoreName(self.store,self.args.tag) # path of store below the mountpoint + self.StorePath = args.config.getStorePath(self.store,self.args.tag) # whole path from / to directory where the snapshot lives + self.BkpPath = args.config.getStorePath('BKP',self.args.tag) # whole path from / to directory where the snapshot lives + self.svols = dict() + self.args = args + self.scanfs() + + def scanfs(self): + logger.warn('SCAN btrfs-drive: %s' % (self.StorePath)) + if self.single: + cmd=['btrfs','subvolume','list','-R','-u','-q','-c','-o',self.StorePath] + else: + cmd=['btrfs','subvolume','list','-R','-u','-q','-c',self.StorePath] + + output = self.args.config.remotecommand(tag=self.args.tag, store=self.store, cmd=cmd) + try: + for line in output.splitlines(): + # btrfs-subvolume list 4.13 output field separator is not anymore only one space. uuid-fields are filled up with whitespaces, so split(' ') does not work anymore!!! + #argmts = line.split(' ') + argmts = line.split() + #ID 2412 gen 8547 cgen 8547 top level 2393 parent_uuid 7991115b-8a6b-6d4d-b664-03db01e902d0 received_uuid - uuid 368490e7-5aca-0d4d-9b7a-becff0487ebd path aldebaran/__ALWAYSCURRENT__.2016-10-15_22:40:25.hourly.part/var-spool-dovecot + self.svols[argmts[1]] = dict() + self.svols[argmts[1]]['id'] = argmts[1] + self.svols[argmts[1]]['gen'] = argmts[3] + self.svols[argmts[1]]['cgen'] = argmts[5] + self.svols[argmts[1]]['tlid'] = argmts[8] + self.svols[argmts[1]]['puuid'] = argmts[10] + self.svols[argmts[1]]['ruuid'] = argmts[12] + self.svols[argmts[1]]['uuid'] = argmts[14] + self.svols[argmts[1]]['path'] = ' '.join(argmts[16:]) + except: + print("Subvolume not found: %s" %(self.StorePath)) + + def DEBUG(self,*msg,level=0): + if self.verbose >= level : + for i in msg: print(i) + return + + def _ret(self,ret,reverse=False): + return(reversed(ret) if reverse else ret) + + def build_tree(self,snap=None,st="/"): + if TREE: + wood = Node(self.config.getStorePath(st,args.tag)+' ('+st+')') + stroot="wood" + isbkp = self.config.getStorePath(st,args.tag) == self.config.getStorePath('BKP',args.tag) + for sub in sorted(self.svols.keys(),key=int): + if self.svols[sub]['path'] == snap: stroot = self.svols[sub]['uuid'] + ext = " (gen: "+str(self.svols[sub]['gen'])+" cgen: "+str(self.svols[sub]['gen'])+")" + try: + vars()[str(self.svols[sub]['uuid'])] = Node(str(self.svols[sub]['path'])+ext,parent=vars()[self.svols[sub]['puuid']]) + except: + vars()[str(self.svols[sub]['uuid'])] = Node(str(self.svols[sub]['path'])+ext,parent=wood) + + for pre, fill, node in RenderTree(wood if snap == None else vars()[stroot]): + print("%s%s" % (pre, node.name)) + else: + print("""anytree is not available. Please ask your sysadmin to install anytree with + pip3 install anytree""") + + + def list_sisters(self,ID,rev=False,older=None,younger=None,names=True): + result = [] + if older == None: older = self.args.older + if younger == None: younger = self.args.younger + for sub in sorted(self.svols.keys(),key=int): + if self.svols[sub]['puuid'] == self.svols[ID]['puuid'] and not sub == ID: + if older: + if int(self.svols[sub]['gen']) >= int(self.svols[ID]['gen']): continue + if younger: + if int(self.svols[sub]['gen']) <= int(self.svols[ID]['gen']): continue + logger.debug('SISTER',self.svols[sub]['id'],self.svols[sub]['path']) + result.append(self.svols[sub]['path']) if names else result.append(sub) + return(self._ret(result,reverse=rev)) + + def list_parent(self,puuid,rev=False,names=True): + result = [] + for sub in sorted(self.svols.keys(),key=int): + if self.svols[sub]['uuid'] == puuid: + logger.debug('PARENT',self.svols[sub]['path']) + result.append(self.svols[sub]['path']) if names else result.append(sub) + return(self._ret(result,reverse=rev)) + + def list_snapshots(self,uuid,rev=False,names=True): + result = [] + for sub in sorted(self.svols.keys(),key=int): + if self.svols[sub]['puuid'] == uuid: + logger.debug('PARENTX',names,sub,self.svols[sub]['path'] if names else sub,"Y") + result.append(self.svols[sub]['path']) if names else result.append(sub) + return(self._ret(result,reverse=rev)) + + def grep_volume(self,expr,names=True,rev=False): + result = [] + for sub in sorted(self.svols.keys(),key=int): + if expr.search(self.svols[sub]['path']): + logger.debug('REGEX',self.svols[sub]['path']) + result.append(self.svols[sub]['path']) if names else result.append(sub) + return(self._ret(result,reverse=rev)) + + def grep_subvolumes(self,expr,names=True,rev=False): + result = [] + for sub in sorted(self.svols.keys(),key=int): + if expr.search(self.svols[sub]['path']): + logger.debug('REGEX',self.svols[sub]['path']) + result.append(self.svols[sub]['path']) if names else result.append(sub) + return(self._ret(result,reverse=rev)) + + def _lssub(self,id): + # recursiv BtrfsListing of all subvolumes below a given snapshot + result = [] + for i in self.svols: + if self.svols[i]['tlid'] == id: + result.append(self.svols[i]['id']) + for i in self._lssub(self.svols[i]['id']): + result.append(i) + return(result) + + def list_subvolumes(self,id,rev=False,names=True,incl_self=True): + result= [] + first = '' if names else id + if incl_self: result.append(first) + for sub in self._lssub(id): + result.append(self.svols[sub]['path'].partition(self.svols[id]['path']+'/')[2]) if names else result.append(sub) + return(self._ret(result,reverse=rev)) + + def count_subvolumes(self,id,incl_self=True): + result = 0 + result = len(self._lssub(id)) + return(result if incl_self else result + 1) + + def get_oldest(self,ID,rev=False,older=None,younger=None,names=True): + result = [] + if older == None: older = self.args.older + if younger == None: younger = self.args.younger + for sub in sorted(self.svols.keys(),key=int): + if self.svols[sub]['puuid'] == self.svols[ID]['puuid'] and not sub == ID: + if older: + if self.svols[sub]['gen'] >= self.svols[ID]['gen']: continue + if younger: + if self.svols[sub]['gen'] <= self.svols[ID]['gen']: continue + logger.debug('SISTER',self.svols[sub]['id'],self.svols[sub]['path']) + #self.DEBUG('SISTER',self.svols[sub]['id'],self.svols[sub]['path'],level=5) + result.append(self.svols[sub]['path']) if names else result.append(sub) + return(self._ret(result,reverse=rev)) + + + def _isbtrfs(self,path,store='SRC',tag='DEFAULT'): + mi = MountInfo(conn=self.args.config.getssh(tag,store)) + logger.info("Filesysem for »%s« is %s" % (path,mi.fstype(path))) + #self.DEBUG("Filesysem for »%s« is %s" % (path,mi.fstype(path)),level=2) + return True if mi.fstype(path) == 'btrfs' else False + + def main(self): + self.scanfs() + +class SubVolumeInfo(BtrfsListing): + + def __init__(self,args,name,store='SRC',single=False): + super().__init__(args,store=store,single=single) + if store == 'SRC': + self.SourceName = os.path.basename(name.rstrip('/')) + else: + self.SourceName = name.strip('/') + self.vol = name + self.uuid = '' # UUID + self.puuid = '' # Parent-UUID + self.ruuid = '' # Received UUID + self.ctime = '' # Creation Time + self.id = 0 # subvolume-ID + self.gen = 0 # Generation + self.cgen = 0 # Generation at creationtime + self.pid = 0 # Parent ID + self.tlid = 0 # Top level ID + self.flags = 0 # Flags + self.snapshots = [] # List of snapshots made from this subvolume + self.translate={'Name':'SourceName', + 'UUID':'uuid', + 'Parent UUID':'puuid', + 'Received UUID':'ruuid', + 'Creation time':'ctime', + 'Subvolume ID':'id', + 'Generation':'gen', + 'Gen at creation':'cgen', + 'Parent ID':'pid', + 'Top level ID':'tlid', + 'Flags':'flags', + 'Snapshot(s)':'snapshots'} + self.parse_btrfs_show() + + def parse_btrfs_show(self): + logger.info('[II] parse subvolume store<%s> %s' % (self.store,self.SourceName)) + #self.DEBUG('[II] parse subvolume store<%s> %s' % (self.store,self.SourceName),level=2) + subvolume_data = dict() + if self.SourceName.startswith(self.StoreName): + SourceName = re.sub(self.StoreName+'/','',self.SourceName) + else: + SourceName = self.SourceName + cmd = ['btrfs','subvolume','show',self.StorePath+'/'+SourceName] + sv = False + snaps=[] + output = self.args.config.remotecommand(tag=self.args.tag, store=self.store, cmd=cmd) + + try: + for line in output.splitlines(): + #argmnts = [arg.strip() for arg in str(line, encoding='utf8').split(': ')] + argmnts = [arg.strip() for arg in line.replace('\t',' ').split(': ')] + + if len(argmnts) > 1: + #setattr(self, self.tr_att(argmnts[0].strip()), argmnts[1].strip()) + setattr(self, self.tr_att(argmnts[0]), argmnts[1]) + elif sv: + snaps.append(argmnts[0].strip()) + else: + if argmnts[0].strip() == 'Snapshot(s):': + sv = True + else: + self.path=argmnts[0].strip() + self.snapshots = snaps + self.dir = os.path.dirname(self.path) + return + except: + print("Subvolume does not exist: %s" % (self.SourceName)) + + +class SubVolume(SubVolumeInfo): + # read Subclasses: http://www.python-course.eu/python3_inheritance.php + what = 'store all informations about a btrfs-Subvolume' + + def __init__(self, args, name, store='SRC', single=False, mdbslice=100): + super().__init__(args,store=store,name=name,single=single) + self.snapped = False + self.transferred = False + self.exist = True + + self.args = args + self.timestamp = args.timestamp + self.verbose = args.verbose + self.tag = args.tag + self.config = args.config #Config() + self.path = '' + self.dir = '' + self.basename = os.path.basename(name.split('.')[0]) + if args.action == 'rollback': + self.otimestamp = os.path.basename(name.split('.')[1]) + if args.action == 'transfer': + self.trans_ts = name.split('.')[1] + self.trans_tag = name.split('.')[2] + + regexpart = re.compile('\.part') + + self.OrigName = re.sub('\.part$','',self.SourceName) + self.OrigLock = self.OrigName+'.part' + if args.action == 'transfer': + self.OLockFile = '.'+self.basename+'.'+self.trans_ts+'.'+self.trans_tag+'.~lock' + else: + self.OLockFile = '.'+self.OrigName+'.~lock' if args.action != 'transfer' else '.'+self.SourceName+'.~lock' + if args.action == 'rollback': + print('rollback',self.OrigName,self.basename,self.otimestamp) + self.SnapName = self.basename+'.rollback-from-'+self.otimestamp+'-on-'+self.timestamp+'.'+"current" + self.SysVol = MountInfo().relpath('/') + self.SysVolOld = self.SysVol.split('.')[0]+'.'+self.timestamp+'.oldcurrent' + else: + self.SnapName = self.basename+'.'+self.timestamp+'.'+self.tag + self.SnapLock = self.SnapName+'.part' + self.SLockFile = '.'+self.SnapName+'.~lock' + self.SnapID = None + logger.warn('OrigName : %s' % (self.OrigName)) + logger.warn('SourceName: %s' % (self.SourceName)) + logger.warn('OrigLock : %s' % (self.OrigLock)) + logger.warn('OLockFile : %s' % (self.OLockFile)) + logger.warn('SnapName : %s' % (self.SnapName)) + logger.warn('SnapLock : %s' % (self.SnapLock)) + logger.warn('SLockFile : %s' % (self.SLockFile)) + + self.parent = '' + self.subvolsshort = [] + self.subvolumes = [] + self.stderr = args.stderr + self.DEBUG('[II] <%s> is %s: ' % (self.store, self.config.getStorePath(self.store,self.args.tag,original=True)),level=2) + #self.partstep = (100 / (self.args.mdbpart * self.args.mdbsteps)) / len(self.list_subvolumes(self.id)) + self.partstep = mdbslice / len(self.list_subvolumes(self.id)) + logger.debug("""Steps + mdpart: %i + count subvolumes: %i + len list_subvolumes: %i + mdparts (mdbpart * mdbsteps): %i + pparts: %i + partstep: %f""" % (self.args.mdbpart, + self.count_subvolumes(self.id), + len(self.list_subvolumes(self.id)), + self.args.mdbpart * self.args.mdbsteps, + 100, + self.partstep)) + #100 / (self.args.mdbpart * self.args.mdbsteps), + + def _createlockfile(self,lf,ln): + #print(self.args.config.getssh(self.tag,self.store)) + if Myos().path_isfile(self.store+'/'+lf,conn=self.args.config.getssh(self.tag,self.store)): + self.DEBUG('Nothing to lock',level=2) + else: + self.DEBUG("%-12s»<%s>/%s«" % (' =lock>',self.store,ln),level=0) + try: + lockfile = open(self.StorePath+'/'+lf, 'w', 1) + lockfile.write(str(os.getpid())) + lockfile.close() + except OSError as e: + print('ERROR create lockfile: %s' % (e)) + pass + return + + def _deletelockfile(self,lf,ln,checked=True): + path = self.StorePath+'/' + if checked: + if check_lockfile(self.args,path+lf): + self.DEBUG('ERROR - lockfile in use: <%s - %s>/%s' % (self.store,self.StorePath,path+lf),level=2) + return + try: + Myos(dry=self.args.dry_run).remove(path+lf,args.config.getssh(self.tag,self.store)) + except OSError as e: + if e.errno == errno.EEXIST or errno.ENOENT: + pass + # if lockfile doesn't exist, continue + #self.DEBUG('INFO - lockfile is not existing: <%s - %s>/%s' % (self.store,self.StorePath,path+lf),level=2) + else: + raise e + return + + + def _rename(self,Action,From,To,store): + if Action == 'rollback': + for i in self.config.getVolumes(): + if i.split('.')[0] == From.split('.')[0].lstrip('/'): + #print('rollback',Action,'Vol %s: From: %s To: %s' % (i,From,To)) +# raise SystemVolumeError(action=Action) + pass + #return + else: + for i in self.config.getVolumes(): + # print('NOTrollback',Action,'Vol %s: From: %s To: %s' % (i,From,To)) + if From == i: + raise RuntimeError('ERROR - Systemvolume not allowed to rename (From): %s' % (From),1) + elif To == i: + raise RuntimeError('ERROR - Systemvolume not allowed to rename (To): %s' % (To),1) + else: + self.DEBUG('Action "%s" (%s) From: %s or To: %s is ok' % (Action,i,From,To),level=3) + + st = self.store if store == None else store + + StorePath = self.config.getStorePath(st,self.args.tag) + if not issubvol(self.args,StorePath,store,self.args.tag): + self.DEBUG("Failed to rename »<%s>%s«, is no btrfs-subvolume, or not existing" % (st,From),level=3) + return + + self.DEBUG(' =try-to-rename> »<%s>/%s« --> »%s«' % (st,From,To),level=3) + pass + + self.DEBUG(" =unlock> <%s>/%s" % (st,To),level=1) + if Myos().path_exists(StorePath+'/'+To,self.args.config.getssh(self.tag,st)): + self.DEBUG(' =%-10s»%s« exists. Nothing to rename' % (Action+':',To),level=3) + return + else: + if self.args.action == 'create': + if Action == 'lock': + self.DEBUG(" =%-11s nothing to rename" % (Action+':'),level=3) + return + else: + if issubvol(self.args,StorePath+'/'+From,store,self.args.tag): + self.DEBUG(' =%s-rename> »<%s>/%s« --> %s' % (Action,st,From,To), level=1) + else: + return + try: + #print('DRY',self.args.dry_run) + Myos(dry=self.args.dry_run).rename(StorePath+'/'+From,StorePath+'/'+To,self.args.config.getssh(self.tag,store)) + except FileNotFoundError: + self.DEBUG('Nothing to rename',level=3) + except OSError as e: + print(e) + except: + raise + + return + + def lock(self,store=None): + store = self.store + logger.error('try to lock on action %s' % (self.args.action)) + #self.DEBUG('try to lock on action %s' % (self.args.action),level=4) + try: + self.DEBUG('Action is %s' % (self.args.action),level=3) + if self.args.action == 'create': + self.DEBUG(' ++> create lockfile for Snapshot (%s)' % (self.args.action),level=3) + self._createlockfile(self.SLockFile,self.SnapName) + pass + elif self.args.action == 'delete' or self.args.action == 'cleanup': + self.DEBUG(' --> delete lockfile for Original (%s)' % (self.args.action),level=3) + self._deletelockfile(self.OLockFile,self.SourceName) + + self._rename('lock',self.SourceName,self.OrigLock,store) + pass + elif self.args.action == 'transfer': + self.DEBUG(' --> create lockfile for Original (%s)' % (self.args.action),level=3) + self._createlockfile(self.OLockFile,self.SourceName) + self.DEBUG(' --> rename to OrigLock',level=3) + self._rename('lock',self.SourceName,self.OrigLock,store) + pass + else: + self.DEBUG(' ++> create lockfile for Snapshot (%s)' % (self.args.action),level=3) + self._createlockfile(self.SLockFile,self.SnapName) + self.DEBUG(' --> rename to SnapLock',level=3) + self._rename('lock',self.SnapName,self.SnapLock,store) + pass + except RuntimeError as e: + if e.args[1] > 0: + raise e + except: + raise + self.scanfs() + return + + def unlock(self,checked=False,store=None): + store = self.store + self.DEBUG('try to unlock on action %s' % (self.args.action),level=4) + try: + if self.args.action == 'create': + #print('A') + self.DEBUG(' --unlock-> %s' % (self.args.action),level=4) + self.DEBUG(' --> delete lockfile from Snapshot (%s)' % (self.args.action),level=3) + self._deletelockfile(self.SLockFile,self.SnapName,checked=False) + self.DEBUG(' --> rename to SnapName',level=3) + self._rename('unlock',self.SnapLock,self.SnapName,store) + self._rename('unlock',self.SnapLock,self.SnapName,'BKP') + pass + elif self.args.action == 'delete': + #print('B') + self.DEBUG(' --> delete lockfile from Original (%s)' % (self.args.action),level=3) + self._deletelockfile(self.OLockFile,self.SourceName,checked=False) + self.DEBUG(' --> rename to OrigName',level=3) + self._rename('unlock',self.OrigLock,self.OrigName,store) + pass + elif self.args.action == 'transfer': + #print('C') + self.DEBUG(' --> delete lockfile from Original (%s)' % (self.args.action),level=3) + self._deletelockfile(self.OLockFile,self.SourceName,checked=False) + self.DEBUG(' --> (%s) rename to OrigName' % (self.args.action),level=3) + self._rename('unlock',self.OrigLock,self.OrigName,store) + self._rename('unlock',self.OrigLock,self.OrigName,'BKP') + pass + if self.args.action == 'rollback': + #print('D') + self.DEBUG(' --unlock-> %s' % (self.args.action),level=4) + self.DEBUG(' --> delete lockfile from Snapshot (%s)' % (self.args.action),level=3) + self._deletelockfile(self.SLockFile,self.SnapName,checked=False) + self.DEBUG(' --> rename to SnapName',level=3) + #print('XXX',self.SnapLock,self.SnapName,self.SysVol,self.SysVolOld) + # rollback snapshot from old snapshot to current volume + self._rename('rollback',self.SnapLock,self.SnapName,store) + # rename current sysvolume to oldcurrent + self._rename('rollback',self.SysVol,self.SysVolOld,store) + #self._rename('unlock',self.SnapLock,self.SnapName,'BKP') + pass + else: + #print('E') + self.DEBUG(' --> delete lockfile from Snapshot (%s)' % (self.args.action),level=3) + self._deletelockfile(self.SLockFile,self.SnapName) + self.DEBUG(' --> rename to SnapName',level=3) + self._rename('unlock',self.SnapLock,self.SnapName,store) + pass + except Exception as e: + DEBUG(e) + raise e + except: + raise + self.scanfs() + return + +# + def tr_att(self,att): + if att in self.translate: + return self.translate[att] + else: + return att + + def setprop(self,ro=True): + if self.args.action == 'create': + sname = self.SnapLock + elif self.args.action == 'setprop': + sname = self.OrigName + else: + sname = self.OrigLock + + for i in self.config.getVolumes(): + if sname == i and ro == True: + raise RuntimeError('ERROR - Systemvolume not allowed to set read-only: %s' % (sname),1) + + propn = 'ro' if ro else 'rw' + prop = 'true' if ro else 'false' + self.DEBUG(' =set-%-6s»<%s>/%s«' % (propn+'>',self.store,sname),level=1) + for sub in self.list_subvolumes(self.id,names=True,rev=True if ro else False): + if issubvol(self.args,self.StorePath+'/'+sname+'/'+sub,self.store,self.args.tag): + self.DEBUG(' =%-9s»<%s>/%s«' % (propn+'>',self.store,sname+'/'+sub),level=1) + try: + cmd = ['btrfs','property','set','-ts',self.StorePath+'/'+sname+'/'+sub,'ro',prop] + self.args.config.remotecommand(tag=self.args.tag, store=self.store, cmd=cmd) + except: + self.DEBUG('setprop error',level=1) + + else: + pass + + def create(self): + self.DEBUG(' =create-snapshot=> from »<%s>/%s«' % (self.store, self.SourceName),level=1) + #compile regular expression for ignoring subvolume-names + if self.args.ignore == None: + if not self.config.getIgnores(self.args.tag) == None: + re_ign = re.compile('|'.join(self.config.getIgnores(self.args.tag).split(','))) + else: + re_ign = None + else: + re_ign = re.compile('|'.join(self.args.ignore)) + logger.debug('Ignores for %s: %s' % (self.tag, re_ign)) + if self.args.npb: pt = progress_timer(description= ' =create-snapshot=> from »<%s>/%s«: ' % (self.store, self.SourceName), n_iter=len(self.list_subvolumes(self.id,names=True))) + if issubvol(self.args,self.StorePath+'/'+self.SourceName,self.store,self.args.tag): + try: + for sub in self.list_subvolumes(self.id,names=True): + # Test if subvolume is in ignorelist - only debug-output now + if re_ign != None and re_ign.search('/'+sub): + self.DEBUG('%-12s»<%s>/%s«:' % (' -IGNORE->',self.store, sub),level=2) + self.args.mdb.update(self.args.tag,self.partstep ) + else: + # Test the Source for local snapshotting + if issubvol(self.args,self.StorePath+'/'+self.SourceName+'/'+sub,self.store,self.args.tag): + dest = self.SnapLock+'/'+sub + #TODO: Test on ignores + cmd = ['btrfs','subvolume','snapshot',self.StorePath+'/'+self.SourceName+'/'+sub,self.StorePath+'/'+dest] + + # Test the destination for local snapshotting + if issubvol(self.args,self.StorePath+'/'+dest,self.store,self.args.tag): + # Test if top level snapshot is existing and a snapshot + self.DEBUG(dest,self.StorePath+'/'+dest,self.store,self.args.tag,level=1) + raise RuntimeError('FAILURE - Destination exists: %s' % (self.StorePath+'/'+dest), 0) + elif issubvol(self.args,self.StorePath+'/'+dest,self.store,self.args.tag) == False: + # If destination exists and is a directory and not a snapshot, delete dir and create snapshot (for subvolumes of snapshot) + self.DEBUG('%-12s»<%s>/%s«' % (' =create>',self.store,dest),level=1) + output = self.args.config.remotecommand(tag=self.args.tag, store=self.store, cmd=['rm','-r','-f',self.StorePath+'/'+dest]) + output = self.args.config.remotecommand(tag=self.args.tag, store=self.store, cmd=cmd) + else: + # If destination doesnt exist, create the snapshot + self.DEBUG('%-12s»<%s>/%s«' % (' =create>',self.store,dest),level=1) + output = self.args.config.remotecommand(tag=self.args.tag, store=self.store, cmd=cmd) + sync() + + elif issubvol(self.args,self.OrigName+'/'+sub,self.store,self.args.tag) == False: + raise RuntimeError('FAILURE - Source is directory, not a subvolume. No creation possible: %s' % (sub), 0) + else: + self.DEBUG( " Source-subvolume not existing: %s" % (self.OrigName+'/'+sub),level=3) + if self.args.npb: pt.update() + self.args.mdb.update(self.args.tag,self.partstep ) + else: + if self.args.npb: pt.update() + self.args.mdb.update(self.args.tag,self.partstep ) + pass + if self.args.npb: pt.finish() + self.snapped = True + + self.scanfs() #rescan filesystem after creating the new snapshots + ex = re.compile(self.SnapLock+'$') + for i in self.grep_volume(ex,names=False,rev=True): + self.DEBUG('SnapID,SnapName %s %s'%(self.svols[i]['id'],self.svols[i]['path']),level=5) + self.SnapID = self.svols[i]['id'] + BtrfsListing.CreatedSubvolumes.push((self.store,self.SnapName)) + return(list([self.svols[i]['id'],self.svols[i]['path']])) + + raise CreateError('FAILURE - create new snapshot failed %s' % (self.SnapLock), 0) + except: + raise CreateError + #return(self.SnapLock) + else: + if self.args.npb: pt.finish() + self.DEBUG('[EE] creation not possible: original not existing: %s' % (self.SourceName),level=0) + raise CreateError('ERROR no snapshot - original not existing: %s' % (self.SourceName)) + if self.args.npb: pt.finish() + + + def transfer(self): + if self.config.getTransfer(self.tag) or self.args.action == 'transfer': + if self.args.action == 'create': + sname = self.SnapLock + sid = self.SnapID + else: + sname = self.OrigLock + sid = self.id + + + #get uuids of SNP/SRC and BKP and compare. If they are the same, no transfer, if they are different, make transfer to external media + #print(self.config.getDevice(store='BKP')) + try: + uuidS = self.config.getUUID(store=self.store,tag=self.args.tag) + except FileNotFoundError: + #self.DEBUG(" |no-transfer: directory for »%s« not existing: »%s«" % (self.store,self.config.getMountPath(store=self.store,tag=args.tag))) + logger.critical(" |no-transfer: directory for »%s« not existing: »%s«" % (self.store,self.config.getMountPath(store=self.store,tag=args.tag))) + return + except: + uuidS = None + + try: + uuidB = self.config.getUUID(store='BKP',tag=self.args.tag) + except FileNotFoundError: + logger.critical(" |no-transfer: directory for »%s« not existing: »%s«" % ('BKP',self.config.getMountPath(store='BKP',tag=args.tag))) + return + except: + uuidB = None + + self.DEBUG("UUID-compare",uuidS,uuidB,level=4) + if None == uuidS: + logger.critical(" |no-transfer: Filesystem for »%s« not mounted: »%s«" % (self.store,self.config.getDevice(store=self.store,tag=args.tag))) + return + if None == uuidB: + logger.critical(" |no-transfer: Filesystem for »%s« not mounted: »%s«" % ('BKP',self.config.getMountPath(store='BKP',tag=args.tag))) + return + + if uuidS == uuidB: + # Same Filesystem on SRC/SNP and BKP. No transfer + logger.critical(" |no-transfer: external backupmedia obviously not mounted") + return + else: + # Test if path to external backup-store is a btrfs-subvolume. if not, no transfer + # first check if BKP-mount is btrfs, then check if BKP-Store exists and is a subvolume. + # If BKP-mount is btrfs, and BKPStore does not exist, create it as subvolume + # If BKPStore exists and is a directory, change it to subvolume + if self._isbtrfs(self.config.getMountPath(store='BKP',tag=args.tag,original=False),'BKP',args.tag): + if Myos().path_exists(self.BkpPath,self.args.config.getssh(self.args.tag,'BKP')): + if issubvol(self.args,self.BkpPath,'BKP',self.args.tag): + logger.debug("BKP-Path %s is subvolume" % (self.BkpPath)) + else: + logger.critical(" |no-transfer: %s should be subvolume, is directory on external media" % (self.BkpPath)) + return +# # TODO move ro-subvolumes to another place... this code is not working +# try: +# print("%s exists; is directory" % (self.BkpPath)) +# print("%s -> change to subvolume, this may take a while" % (self.BkpPath)) +# # rename original BKPStore +# print('A ren') +# cmd = ['mv',self.BkpPath,self.BkpPath+'.orig'] +# sp_call(self.args,cmd) +# # Create a new Subvolume +# print('B cr') +# cmd = ['btrfs','subvolume','create',self.BkpPath] +# sp_call(self.args,cmd) +# # move content to new snapshot +# print('C mv') +# cmd = ['mv',self.BkpPath+'.orig/*',self.BkpPath] +# sp_call(self.args,cmd) +# # delete old directory +# print('D rm') +# cmd = ['rm','-rf',self.BkpPath+'.orig'] +# sp_call(self.args,cmd) +# print('E') +# except: +# return + + else: + print('%s not exists -> create subvolume' % (self.BkpPath)) + #self.DEBUG('isdir ==> delete ==> transfer',self.BkpPath,level=3) + logger.debug('isdir ==> delete ==> transfer',self.BkpPath) + cmd = ['btrfs','subvolume','create',self.BkpPath] +# sp_call(self.args,cmd) + self.args.config.remotecommand(tag=self.args.tag, store='BKP', cmd=cmd) + else: + self.DEBUG(' |no-transfer: no subvolume for backup on external media') + return + + BKP = BtrfsListing(self.args,store='BKP') + transfers = dict() + regexpart = re.compile('\.part$|\.part/') + + if self.args.action == 'transfer': + self.scanfs() #update btrfs-list in memory + + clones = dict() #clone list + if not self.args.no_clones: + for par in self.grep_volume(re.compile('^'+self.OrigName.split('.')[0]+'\.'),names=False): + for bsub in sorted(BKP.svols): + if BKP.svols[bsub]['path'] == self.svols[par]['path'] and not regexpart.search(self.svols[par]['path']): + subbasename = re.sub('^'+self.svols[par]['path'].split('/')[0],self.basename,self.svols[par]['path']) + if not subbasename in clones: + clones[subbasename] = list() + clones[subbasename].append(self.svols[par]['path']) + self.DEBUG("Add to clones",self.svols[par]['path'],"->",subbasename,level=4) + + for sub in self.list_subvolumes(sid,names=False): + plist = dict() #parent list + for sis in self.list_sisters(self.svols[sub]['id'],older=True,younger=False,names=False,rev=False): + for bsub in sorted(BKP.svols): + if BKP.svols[bsub]['ruuid'] == self.svols[sis]['uuid']: + plist[self.svols[sis]['id']] = self.svols[sis]['id'] + + + transfers[self.svols[sub]['path']] = dict() + if len(plist) > 0: + v=[int(i) for i in plist.values()] + k=list(plist.keys()) + parent = k[v.index(max(v))] + transfers[self.svols[sub]['path']]['parent'] = self.svols[parent]['path'] + self.DEBUG(self.svols[parent]['path'],"id: ",k,level=3) + else: + pass + + if len(clones) > 0: + bsubvol = re.sub('^'+self.svols[sub]['path'].split('/')[0], + self.basename, + self.svols[sub]['path']) + if bsubvol in clones: + transfers[self.svols[sub]['path']]['clone'] = clones[bsubvol] + + self.DEBUG(' ==transfer=> »<%s>/%s«' % (self.store,sname),level=1) + if self.args.npb: pt = progress_timer(description= ' ==transfer=> »<%s>/%s«: ' % (self.store,sname), n_iter=len(transfers.keys())*2) + for t in sorted(transfers.keys()): + dest = args.config.getStorePath('BKP',args.tag)+'/'+t + destdir = os.path.dirname(dest.rstrip('/'))+'/' + if issubvol(self.args,dest,self.store,self.args.tag) == False: + if Myos().path_exists(dest,self.args.config.getssh(self.args.tag,'BKP')): + self.DEBUG('isdir ==> delete ==> transfer',dest,level=3) + cmd = ['rm','-r','-f',dest] + #sp_call(self.args,cmd) + self.args.config.remotecommand(tag=self.args.tag, store='BKP', cmd=cmd) + else: + pass + self.DEBUG('notexist ==> transfer', dest,level=3) + else: + self.DEBUG('ignored: subvolume »%s« exists in »%s«' % (t,destdir),level=2) + continue + + try: + self.send_receive( + t, + destdir, + parent=transfers[t]['parent'] if 'parent' in transfers[t] else None, + clone=transfers[t]['clone'] if 'clone' in transfers[t] else None) + self.transferred = True + except TransferError: + raise TransferError + + if self.args.npb: pt.update() + self.args.mdb.update(self.args.tag,self.partstep ) + try: + cmd = ['btrfs','property','set','-ts',dest,'ro','false'] + X = self.args.config.remotecommand(tag=self.args.tag, store='BKP', cmd=cmd) + except: + raise TransferError + + BtrfsListing.TransferedSubvolumes.push(re.sub('\.part$','',sname)) + for t in reversed(sorted(transfers.keys())): + dest = args.config.getStorePath('BKP',args.tag)+'/'+t + destdir = os.path.dirname(dest.rstrip('/'))+'/' + self.DEBUG(" =set-ro> %s" % (dest),level=1) + try: + cmd = ['btrfs','property','set','-ts',dest,'ro','true'] + #sp_call(self.args,cmd,level=2) + self.args.config.remotecommand(tag=self.args.tag, store='BKP', cmd=cmd) + if self.args.npb: pt.update() + except: + raise TransferError + if self.args.npb: pt.finish() + return(sname) + + def send_receive(self,source,dest,parent=None,clone=None): + tclones, tparent = list(), list() + if parent == None: + if clone == None: + self.DEBUG('%-20s»<%s>/%s«' % (' =initial transfer=>',self.store,source),level=1) + else: + for x in sorted(clone): + tclones.append('-c') + tclones.append(self.StorePath + '/' + x) + self.DEBUG('%-20s»<%s>/%s« <-clone: %s' % (' =incr-clone transfer=> ',self.store,source,' '.join(tclones)),level=1) + else: + if clone != None: + self.DEBUG('%-20s»<%s>/%s« <-parent: %s + clones' % (' =incr transfer=> ',self.store,source,parent),level=1) + for x in sorted(clone): + tclones.append('-c') + tclones.append(self.StorePath + '/' + x) + else: + self.DEBUG('%-20s»<%s>/%s« <-parent: %s' % (' =incr transfer=> ',self.store,source,parent),level=1) + tparent = ['-p',self.StorePath+'/'+parent] + + first = ['btrfs','send'] + tclones + tparent + [self.StorePath+'/'+source] + second = ['btrfs','receive',dest] + + ssh = self.args.config.getSsh(self.tag) + if ssh[self.store] != None: + send=ssh[self.store]['user']+'@'+ssh[self.store]['host'] + first = ['ssh','-p',str(ssh[self.store]['port']),send]+first + else: + send='local' + + if ssh['BKP'] != None: + recv=ssh['BKP']['user']+'@'+ssh['BKP']['host'] + second = ['ssh','-p',str(ssh['BKP']['port']),recv]+second + else: + recv='local' + + + #output = args.config.remotecommand(tag=args.tag, store=store, cmd=cmd) + + if not self.args.transferinfo: +# print("S->R",first,second) +# stdin_send,stdout_send,stderr_send = cmdsh(self.tag,self.store,first) +# stdin_recv,stdout_recv,stderr_recv = cmdsh(self.tag,'BKP',second) + try: + self.DEBUG(' '.join(first)+' | '+' '.join(second),level=3) + process_send = subprocess.Popen(first, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + shell=False) + process_recv = subprocess.Popen(second, stdin=process_send.stdout, stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, shell=False) + # Allow process_curl to receive a SIGPIPE if process_send exits. + process_send.stdout.close() + out, err = process_recv.communicate() + #self.DEBUG("out --- err",out," --- ",err,level=3) + except: + raise TransferError('ERROR - sending to external backup-media failed') + pass + return(out.decode('utf8').split('\n')) + else: + self.DEBUG('no transfer - only information') + return + + def symlink_force(self,target, link_name, store): + cmd=['ln','-s','-f', '-n', target, link_name] + self.args.config.remotecommand(tag=self.args.tag, store=store, cmd=cmd) + self.DEBUG("[II] TARGET => LINKNAME: %s => %s" % (target.strip(),link_name.strip()),level=3) + +# try: +# os.symlink(target, link_name) +# except OSError as e: +# if e.errno == errno.EEXIST: +# os.remove(link_name) +# os.symlink(target, link_name) +# self.DEBUG("[II] TARGET => LINKNAME: %s => %s" % (target.strip(),link_name.strip()),level=3) +# else: +# raise e + + def symlink(self): + for st in list(self.args.sourcepath.keys())[0], list(self.args.destpath.keys())[0]: + StorePath = self.config.getStorePath(st,self.args.tag) + if not issubvol(self.args,StorePath,st,self.args.tag): continue + linkName = self.basename+'.'+args.config.getSymLink(args.tag) + self.DEBUG('%-12s =>»<%s>/%s«' % (' =symlink>',st,linkName),level=0) + self.symlink_force('./'+self.SnapName, StorePath+'/'+linkName,st) + if args.config.getSymLink(args.tag) != args.config.getSymLink(): + self.DEBUG('%-12s =>»<%s>/%s«' % (' =symlink>',st,self.basename+'.LAST'),level=0) + self.symlink_force('./'+self.SnapName, StorePath+'/'+self.basename+'.LAST',st) + + def symlink_sysvol(self): + for st in list(self.args.sourcepath.keys())[0], list(self.args.destpath.keys())[0]: + StorePath = self.config.getStorePath(st,self.args.tag) + if not issubvol(self.args,StorePath,st,self.args.tag): continue + #linkName = self.basename+'.'+args.config.getSymLink(args.tag) + linkName = self.basename + self.DEBUG('%-12s =>»<%s>/%s«' % (' =symlink>',st,linkName),level=0) + self.symlink_force('./'+self.SnapName, StorePath+'/'+linkName,st) +# if args.config.getSymLink(args.tag) != args.config.getSymLink(): +# self.DEBUG('%-12s =>»<%s>/%s«' % (' =symlink>',st,self.basename+'.LAST'),level=0) +# self.symlink_force('./'+self.SnapName, StorePath+'/'+self.basename+'.LAST',st) + + def delete(self): + self.DEBUG('%-20s from »<%s>/%s«' % (' =delete-snapshot=>',self.store, self.SourceName),level=1) + if self.args.action == 'somethingelse': + sname = self.SnapLock + slock = self.SLockFile + sid = self.SnapID + else: + sname = self.OrigLock + slock = self.OLockFile + sid = self.id + + + #print('delete %s - %s with %s and action %s' % (sname,sid,slock,self.args.action)) + + for i in self.config.getVolumes(): + #print(i,sname,i+'.part') + if ( sname == i or sname == i+'.part' ): + raise RuntimeError('ERROR - Systemvolume not allowed to delete: %s' % (sname),1) + return + + if self.args.npb: pt = progress_timer(description= ' =delete-snapshot=> from »<%s>/%s«: ' % (self.store, self.SourceName), n_iter=len(self.list_subvolumes(self.id,names=True))) + delsubs = [] + if issubvol(self.args,self.StorePath+'/'+sname,self.store,self.args.tag): + for sub in self.list_subvolumes(sid,names=True,rev=True,incl_self=True): + issub = issubvol(self.args,self.StorePath+'/'+sname+'/'+sub,self.store,self.args.tag) + if issub == None: + self.DEBUG('del - not existing: '+self.StorePath+'/'+sname+'/'+sub,level=3) + pass + else: + if issub: + if not Myos().path_isfile(self.StorePath+'/'+slock,self.args.config.getssh(self.tag,self.store)): + self.DEBUG(' -prepare-to-delete> <%s>/%s' % (self.store,sname+'/'+sub),level=1) + delsubs.append(self.StorePath+'/'+sname+'/'+sub) + else: + self.DEBUG('del - subvolume is locked: '+self.dir+'/'+sname+'/'+sub,level=3) + pass + else: + self.DEBUG('del - dir: '+self.dir+'/'+sname+'/'+sub,level=3) + pass + + if self.args.action == 'create' or self.args.action == 'cleanup': + self.setprop(ro=False) + if len(delsubs) > 0: + cmd = ['btrfs','subvolume','delete','-c'] + cmd.extend(delsubs) + self.DEBUG(' -delete-subvolumes>',level=1) + try: + #sp_call(self.args,cmd) + self.args.config.remotecommand(tag=self.args.tag, store=self.store, cmd=cmd) + self.DEBUG(' -subvolumes-deleted>',level=1) + #linkName = self.basename+'.'+args.config.getSymLink(args.tag) + BtrfsListing.DeletedSubvolumes.push((self.store,re.sub('\.part$','',sname))) + self.del_vol = self.OrigName.split('.')[0] + self.del_ts = self.OrigName.split('.')[1] + self.del_tag = self.OrigName.split('.')[2] + except: + raise DeleteError + + else: + self.DEBUG('del - no subvolumes registered',level=0) + if self.args.npb: pt.update() + + if self.args.npb: pt.finish() + + + def main(self): + #print(self.svols) + #print(self.list_sisters(self.ID)) + #try: + self.parse_btrfs_show() + # self.read_subvolumes(self.storename+'/'+self.name) + #except: + # print("Snapshot does not exist: <%s>/%s" % (self.store,self.name)) + # return(self.exist) + +def main(args): + for st in args.store: + print('S',args.config.getStorePath(st,args.tag)) + for s in args.snapshots: + print(st,s) + + L=SubVolume(args,s,store=st) + L.list_sisters(L.id,rev=True) + #L.main() + #L.list_sisters() + #X=SubVolume(BtrfsListing) + +def issubvol(args,path,store='SRC',tag=None): + #print("issubvol-store",store,tag,path) + if not path == None: + cmd = ['stat', '-c', '%i', path ] + try: + return True if int(args.config.remotecommand(tag=args.tag, store=store, cmd=cmd, stderr=subprocess.DEVNULL)) == 256 else False + except: + return False + return True if int(output) == 256 else False + + + +def cleanup(args): + args.mdbsteps = 4 #create, setprop, transfer, cleanup + args.mdbslice = 100 / 4 + DEBUG(' --== cleanup ==-->',verbose=args.verbose,level=1) + #self.partstep = (100 / (args.mdbpart * args.mdbsteps)) / len(self.list_subvolumes(self.id)) + if args.npb: pt = progress_timer(description= ' --== cleanup ==-->', n_iter=6) + args.action = 'cleanup' + #DEL = dict() + DEL = Stack() + ST = dict() + explock = re.compile('.*.~lock$') + exppart = re.compile('.part$') + partlock = dict() + cI,I = dict(), dict() + for i in args.config.ListIntervals(): + cI[i] = 0 + I[i] = args.config.getInterval(i) + #for st in args.store: + for st in ['SRC','SNP','BKP']: + StorePath = args.config.getStorePath(st,args.tag) + if not issubvol(args,StorePath,st,args.tag): + DEBUG(args,' =cleanup: store doesnt exist: %s' % (StorePath),level=3) + continue + if args.npb: pt.update() + else: + DEBUG(args,' -Cleanup store <%s>:%s' % (st,StorePath),level=3) + StorePath = args.config.getStorePath(st,args.tag)+'/' + ST[st] = BtrfsListing(args,store=st,single=True) + if len(args.snapshots) > 0: + DEBUG(args,' --cleanup interval-snapshots',level=3) + for sn in [v.partition('.current')[0] for v in args.snapshots]: + #for sn in args.snapshots: + ex = re.compile('$|'.join([sn+'\..*\.' + word_in_list for word_in_list in I.keys()])+'$') + DEBUG(args,'Regex:',ex,level=5) + for i in args.config.ListIntervals(): + cI[i] = 0 + try: + for i in ST[st].grep_subvolumes(ex,names=False,rev=True): + for tag in I.keys(): + if ST[st].svols[i]['path'].partition('.'+tag)[1] != '': + cI[tag] += 1 + if cI[tag] > int(I[tag]): + DEL.push([st,ST[st].svols[i]['path']]) + except: + raise CleanupError('cleanup failed for %s' % (sn)) + + # Find unused lockfiles and delte them also + DEBUG(args,' --cleanup *.part$-Snapshots',level=3) + for i in ST[st].svols: + subvol = ST[st].svols[i]['path'] + if exppart.search(subvol): + DEL.push([st,subvol]) + + # Delete them + while not DEL.is_empty(): + print("DEL len: ", DEL.len()) + S = DEL.pop() + try: + L=SubVolume(args,S[1],store=S[0], mdbslice=args.mdbslice / (DEL.len() + 1)) + L.lock() + L.delete() + except NoSubvolumeError: + continue + except: + raise + if args.npb: pt.update() + + DEBUG(args,' --cleanup unused lockfiles',level=3) + for lf in Myos().listdir(args.config.getStorePath(st,args.tag),args.config.getssh(args.tag,st)): + if explock.search(lf): + if check_lockfile(args,lf): + pass + elif check_lockfile(args,lf) == None: + pass + else: + DEBUG(' --> remove unused lockfile: '+lf,level=1,verbose=args.verbose) + Myos(dry=self.args.dry_run).remove(lf,args.config.getssh(args.tag,st)) + if args.npb: pt.update() + + if args.npb: pt.finish() + return + +def create(args): + SRC=dict() + args.mdbsteps = 4 #create, setprop, transfer, cleanup + args.mdbslice = 100 / 4 + for snap in args.snapshots: + try: + SRC[snap] = SubVolume(args,snap,store=list(args.sourcepath.keys())[0], mdbslice=args.mdbslice) + SRC[snap].create() + SRC[snap].lock() + SRC[snap].setprop(ro=True) + SRC[snap].transfer() + SRC[snap].unlock() + SRC[snap].symlink() + except RuntimeError as e: + if e.args[1] == 0: + print('nicht so schlimm') + else: + raise e + except (NoSubvolumeError, NoBtrfsVolumeError): + print("Create Error: %s not found -> continue" % (snap)) + pass + except: + raise + cleanup(args) + return + +def rollback(args): + SRC=dict() + args.mdbsteps = 4 #create, setprop, transfer, cleanup + args.mdbslice = 100 / 4 + for snap in args.snapshots: + try: + SRC[snap] = SubVolume(args,snap,store=list(args.sourcepath.keys())[0]) + SRC[snap].create() + SRC[snap].lock() + SRC[snap].setprop(ro=False) + #SRC[snap].transfer() + SRC[snap].unlock() + SRC[snap].symlink_sysvol() + #SRC[snap].rename() + except RuntimeError as e: + if e.args[1] == 0: + print('nicht so schlimm') + else: + raise e + except (NoSubvolumeError, NoBtrfsVolumeError): + print("Create Error: %s not found -> continue" % (snap)) + pass + except: + raise + cleanup(args) + return + +def delete(args): + args.mdbsteps = 4 #create, setprop, transfer, cleanup + args.mdbslice = 100 / 4 + for st in args.sourcepath.keys(): + SRC=dict() + for snap in args.snapshots: + try: + SRC[snap] = SubVolume(args,snap,store=st) + SRC[snap].lock() + SRC[snap].setprop(ro=False) + SRC[snap].delete() + except RuntimeError as e: + if e.args[1] == 0: + print("delete error - nicht so schlimm") + else: + raise e + except (NoSubvolumeError, NoBtrfsVolumeError): + print("Delete Error: <%s> %s not found -> continue" % (st,snap)) + pass + except: + raise + return + +def setprop(args): + args.mdbsteps = 4 #create, setprop, transfer, cleanup + args.mdbslice = 100 / 4 + for st in args.sourcepath.keys(): + SRC=dict() + for snap in args.snapshots: + try: + SRC[snap] = SubVolume(args,snap,store=st) + SRC[snap].lock() + SRC[snap].setprop(ro=args.ro) + SRC[snap].unlock() + except RuntimeError as e: + print(e.args,e.args[1]) + if e.args[1] == 0: + print("setprop error - nicht so schlimm") + else: + raise e + except (NoSubvolumeError, NoBtrfsVolumeError): + print("Setprop Error: <%s> %s not found -> continue" % (st,snap)) + pass + except: + raise + return + +def lists(args): + args.mdbsteps = 4 #create, setprop, transfer, cleanup + args.mdbslice = 100 / 4 + if args.info: + if args.showpathstore: + for st in args.store: print(args.config.getStorePath(st,args.tag,original=True)) + if args.showtags: + for i in args.config.ListIntervals(): print(i) + if args.showtransfers: + if args.config.getTransfer(intv=args.tag): + DEBUG('"%s" results in "%s", will transfer actual snapshots from this volume(s):' % (args.tag,args.config.getIsDefault(intv=args.tag)),level=1,verbose=args.verbose) + print(' '.join(args.config.getVolumes(tag=args.tag))) + else: + DEBUG('%s will transfer nothing' % (args.tag),level=1,verbose=args.verbose) +# if args.showdb: +# cursor = args.conn.execute("SELECT ID, INTNAME, VOL, SNAPNAME, SNAP_TS, TRNS_TS, DELETED from JOURNAL") +# print("Journal") +# tabstr = "%-4s%-12s %-22s %50s | %-19s - %19s\t%s" +# print( tabstr % ("ID","Interval","Volume", "SNAMPNAME","Last run","Last Trans","Deleted")) +# for row in cursor: +# print(tabstr % (str(row[0])+':',row[1],row[2],'»'+row[3]+'«',row[4],row[5],row[6])) + + if args.print_config: + args.config.PrintConfig(tag = None if args.tagset else args.tag, of=args.of) + + if args.showyoungest: + ST=dict() + for st in args.store: + ST[st]=dict() + #for vol in args.config.getVolumes(): + for vol in args.snapshots if len(args.snapshots) > 0 else '*': + #print('VOL',vol) + ST[st][vol]=dict() + for intv in args.config.ListIntervals(): + ST[st][vol][intv] = dict() + if args.config.getStorePath(st,args.tag) is not None: + dirlist = dict() + for snps in glob.glob(args.config.getStorePath(st,args.tag)+'/'+vol+'.*'): + #print("snps",snps) + if len(os.path.basename(snps).split('.')) == 3: + if not os.path.basename(snps).split('.')[1] in dirlist: + dirlist[os.path.basename(snps).split('.')[1]] = list() + dirlist[os.path.basename(snps).split('.')[1]].append(os.path.basename(snps)) + for t in sorted(dirlist.keys()): + for snp in dirlist[t]: + #print("T",dirlist[t],snp) + for intv in ST[st][vol].keys(): + R = re.compile(intv +'$') + #print("I",intv) + if R.search(snp): + #print("snp",st,vol,intv,snp) + #ST[st][vol][intv].append(os.path.basename(snp)) + #print(intv,snp,os.path.basename(snps).split('.')[1]) + if not os.path.basename(snp).split('.')[1] in ST[st][vol][intv]: + ST[st][vol][intv][os.path.basename(snp).split('.')[1]] = list() + ST[st][vol][intv][os.path.basename(snp).split('.')[1]].append(os.path.basename(snp)) + ST[st][vol]['dirlist'] = dirlist + #for x in ST[st][vol].keys(): print(x,ST[st][vol][x]) + for st in ST.keys(): + fp = '' + if args.shortpath == True: fp='/' + if args.mountpath == True: fp=args.config.getStoreName(st,args.tag)+'/' + if args.fullpath == True: fp=args.config.getStorePath(st,args.tag)+'/' + DEBUG('..--==°°==--..',level=1,verbose=args.verbose) + DEBUG('Youngest snapshot in %s' % (st),level=1,verbose=args.verbose) + #print("ST",ST[st]) + for vol in ST[st].keys(): + s = dict() + missing = list() + DEBUG(' Volume %s:' % (vol),level=1,verbose=args.verbose) + for intv in sorted(ST[st][vol].keys()): + #print(ST[st][vol][intv]) + if intv != 'dirlist': + if len(ST[st][vol][intv]) > 0: + #for x in sorted(ST[st][vol][intv].keys()): print("Key",x,ST[st][vol][intv],ST[st][vol][intv][x]) + #i = ST[st][vol][intv].pop() + #print("X",ST[st][vol][intv][max(ST[st][vol][intv].keys())]) + k = max(ST[st][vol][intv].keys()) + i = ST[st][vol][intv][k] + #print("i",intv,i,k) + if args.tagset: + if args.all: + #print("A") + s[i.split('.')[1]] = i + else: + #print("B") + s['dirlist'] = ST[st][vol]['dirlist'][sorted(dirlist.keys()).pop()] + else: + #print("C",i) + #if i.split('.')[2] == args.tag: + #print("D",i) + #s[i.split('.')[1]] = i + if intv == args.tag: + s[k] = i + else: + missing.append(intv) + + lo = 0 + for i in reversed(sorted(s.keys())): + #for j in s[i] if i == 'dirlist' else [s[i]]: + for j in s[i]: + #out = '»<%s>/ %s%s«' % (st,fp,j) + out = '%s%s«' % (fp,j) + lo = len(out) if len(out) > lo else lo + if args.verbose == 0: + DEBUG('%s' % (out),level=0,verbose=args.verbose) + else: + DEBUG(' %s' % (out),level=1,verbose=args.verbose) + DEBUG(' %s' % ('-' * (lo)),level=1,verbose=args.verbose) + lo = 0 + out = """missing: %s""" % (', '.join(missing)) + + if args.tag == 'misc': DEBUG(""" %s\n %s\n""" % (out,'=' * len(out)),level=1,verbose=args.verbose) + + if not (args.showyoungest or args.showtransfers or args.showdb or args.showtags or args.showpathstore or args.print_config): + reintv = re.compile('NEXT|'+'|'.join(args.config.ListIntervals())) + cmd=['systemctl','list-timers'] + try: + #output,error = sp_co(self.args,cmd) + res = subprocess.Popen(cmd,stdout=subprocess.PIPE, stderr=args.stderr) + output,error = res.communicate() + if res.returncode > 0: + raise + except: + raise + #if output[0] == 0: + cut = 120 + for line in output.splitlines(): + l = line.decode() + if "ACTIVATES" in l: + cut = l.find("ACTIVATES") + i = l.find("UNIT") + print(l[:cut]) + elif reintv.search(l): + l = l[:cut] + o = l.replace("timer-","").replace(".timer","").rstrip() + x = o[i:].rstrip(), + print(o+" backup","\t--> transfer" if args.config.getTransfer(x[0]) else "") + print("--finish--") + + else: + text = "subvolumes in" + if args.snap: + text = "snapshots from" + elif args.sisters or args.older or args.younger: + text = "sisters from" + if args.older: + text = 'older '+text + elif args.younger: + text = 'younger '+text + else: + text = 'all '+text + elif args.tree: + text = "treeview from" + elif args.parents: + text = "parent from" + + for st in args.sourcepath.keys(): + SRC=dict() + if len(args.snapshots) > 0: + for snap in args.snapshots: + try: + SRC[snap]=SubVolume(args,snap,store=st,single=True if args.tree else False) + #print('B',SRC[snap].SnapName,snap,st) + DEBUG("[II] <%s> is %s" %(st,args.config.getStorePath()),level=1,verbose=args.verbose) + DEBUG("[II] list %s <%s>/%s" %(text,st,SRC[snap].SourceName),level=1,verbose=args.verbose) + fp='' + if args.sisters or args.older or args.younger or args.parents or args.snap: + if args.shortpath == True: fp='/' + if args.mountpath == True: fp=SRC[snap].StoreName+'/' + if args.fullpath == True: fp=SRC[snap].StorePath+'/' + else: + if args.shortpath == True: fp='/'+SRC[snap].SourceName+'/' + if args.mountpath == True: fp=SRC[snap].StoreName+'/'+SRC[snap].SourceName+'/' + if args.fullpath == True: fp=SRC[snap].StorePath+'/'+SRC[snap].SourceName+'/' + + if args.snap: + # List children of snapshot + #for i in SRC[snap].list_snapshots(SRC[snap].uuid,rev=args.reverse): print(fp+SRC[snap].SourceName+i) + for i in SRC[snap].list_snapshots(SRC[snap].uuid,rev=args.reverse): print(fp+i) + elif args.sisters or args.older or args.younger: + # List sisters of snapshot (same parent) + for i in SRC[snap].list_sisters(SRC[snap].id,rev=args.reverse): print(fp+i) + elif args.tree: + #print("Treeview of snapshots under %s - in development" % (snap)) + SRC[snap].build_tree(snap=snap,st=st) + elif args.parents: + #print("Parentview of snapshots - in development") + for i in SRC[snap].list_parent(SRC[snap].puuid): print(fp+i) + else: + #DEBUG("List subvolumes in given snapshot %s" % (snap),level=1,verbose=args.verbose) + for i in SRC[snap].list_subvolumes(SRC[snap].id,rev=args.reverse): print(fp+i) + except RuntimeError as e: + print(e.args,e.args[1]) + if e.args[1] == 0: + print("list error - nicht so schlimm") + else: + raise e + except (NoSubvolumeError, NoBtrfsVolumeError): + print("List Error: <%s>/%s not found -> continue" % (st,snap)) + pass + except ScanFsError: + pass + except: + raise + else: + #if args.tree: + print("Treeview of snapshots in %s - in development" % (st)) + SRC['.']=SubVolume(args,'.',store=st,single=True if args.tree else False) + SRC['.'].build_tree(snap='.',st=st) + + return + +def transfer(args): + args.mdbsteps = 4 #create, setprop, transfer, cleanup + args.mdbslice = 100 / 4 + SRC=dict() + for snap in args.snapshots: + try: + SRC[snap] = SubVolume(args,snap,store=list(args.sourcepath.keys())[0]) + SRC[snap].lock() + SRC[snap].transfer() + SRC[snap].unlock(store=list(args.sourcepath.keys())[0]) + except (NoSubvolumeError, NoBtrfsVolumeError): + #print("Transfer Error: <%s> %s not found -> continue" % (st,snap)) + print("Transfer Error: %s not found -> continue" % (snap)) + pass + except: + raise + cleanup(args) + +def restore(args): + args.mdbsteps = 4 #create, setprop, transfer, cleanup + args.mdbslice = 100 / 4 + #print('RESTORE FILES', args) + # if User running "mkbackup restore" is root, set uroot=True + if os.getuid() == 0: + uroot = True + else: + uroot = False + + if args.no_preserve: + # If set no_preserve (-n or --no-preserve), create no backup from destination-file + cpopts=['-b','--suffix=.'+args.timestamp+'.bak'] + + + for f in args.file: + #print('restore',os.getcwd(),f,os.path.realpath(f)) + src = Myos().path_realpath(f,args.config.getssh(args.tag,'SRC')) + bkpsubdir = '/' + mountp = src + while mountp > '/': + cmd=['mountpoint',mountp] + try: + res = subprocess.Popen(cmd,stdout=subprocess.PIPE, stderr=args.stderr) + output,error = res.communicate() + if res.returncode > 0: + bkpsubdir = mountp + mountp = os.path.dirname(mountp) + else: + break + except: + raise + #print(mountp,bkpsubdir,src.replace(bkpsubdir,'')) + mounts = open('/proc/mounts','r') + R = re.compile(' '+mountp+' ') + for line in mounts.readlines(): + if R.search(line): + FS = line.split(' ')[2] + #print(FS,uroot) + if FS == "fuse.MksnapshotFS.py": + if uroot: + dst = src.replace(bkpsubdir,'') + else: + dst = os.environ['HOME']+src.replace(bkpsubdir,'') + else: + if mountp in [args.config.getMountPath('BKP',args.tag,original=False), args.config.getMountPath('SNP',args.tag,original=False)]: + dst = src.replace(bkpsubdir,'') + #print(dst) + else: + dst = '' + print('no backup - no restore') + #print('cp', '-b', '--suffix=.'+args.timestamp+'.bak', src, dst) + if not args.no_preserve: + bkp = dst+'.'+args.timestamp+'.bak~' + try: + os.rename(dst,bkp) + except: + print("Destination-file »%s« not existing -> continue" % (dst)) + pass + #raise + + #print('cp',src,dst) + #print('no copy - do it manually - activate in code') + try: + shutil.copytree(src, dst) + print("File restored from %s: %s" % (src,dst)) + except OSError as exc: # python >2.5 + if exc.errno == errno.ENOTDIR: + shutil.copy(src, dst) + print("File restored from %s: %s" % (src,dst)) + else: raise + #shutil.copy(src,dest) + mounts.close() + +#class desktop_notification: +# def __init__(self, args, urgency=1): +# self.args = args +# self.dbus_path = "/at/xundeenergie/notifications" +# self.dbus_iface = "at.xundeenergie.notifications.advanced" +# self.dbus_busname = "at.xundeenergie.notifications" +# self.timestamp = datetime.datetime.now() +# self.time = self.timestamp.strftime('%H:%M:%S') +# self.date = self.timestamp.strftime('%d. %B %Y') +# self.bus = dbus.SystemBus() +# if int(urgency) == 0: +# self.signal_name = 'Notification_low' +# elif int(urgency) == 1: +# self.signal_name = 'Notification_normal' +# else: +# self.signal_name = 'Notification_critical' +# print('NO',int(urgency),self.signal_name) +# +# def send_signal(self, intv='default', *args): +# """Send a signal on the bus.""" +# msg = dict() +# msg['sender'] = "mkbackup" +# msg['msgheader'] = "%s-backup" % (self.args.tag) +# msg['msgbody'] = """am %s +#um %s Uhr abgeschlossen. +# +#%s +#(Ugency: %s) +#""" % (self.date, self.time, '\n'.join(args), self.signal_name) +# msg['expiration_timeout'] = '-1' +# message = dbus.lowlevel.SignalMessage(self.dbus_path, self.dbus_iface, self.signal_name) +# message.append(msg) +# self.bus.send_message(message) + +# PARSER + +config=Config() + +parser = argparse.ArgumentParser() +parser.add_argument('--version', action='version', version='0.1.0') +parser.add_argument('-V', '--systemvolumes', action='store_true', + default=False, help='''take the systemvolumes from config. store is + always SNP''') +parser.add_argument('-t', '--tag', default=None, help='''one of %s''' % (config.ListIntervals())) +parser.add_argument('-v', '--verbose', action='count', default=0, help='''verbose output''' ) +parser.add_argument('-L', '--logfile', default=None, help='''Write output also to this file in /var/log/''') +#parser.add_argument('-i', '--info', action='store_true', default=False, help='''Print infos, action is "list"''') +parser.add_argument('-i', '--ignore', action='append', help='''Regular expression pattern for ignoring several subvolumes to be not backed up (and subvolumes unter them) + Use it more than once''') +parser.add_argument('-n', '--no-progressbar', dest='npb', action='store_false', default=True, help='''Dont show progressbar (for use in systemctl-unit for example)''') +parser.add_argument('-B', '--backup-mount-path', + dest='bkpmount', + default=config.getMountPath('BKP'), + help='''set path to destination mountpoint of backup-mountpoint. (default=%s) overrides 'BKP-Path':''' % (config.getMountPath('BKP'))) +parser.add_argument('-b', '--backup-store', + dest='bkpstore', + default=config.getStoreName('BKP'), + help='''set storename in backup-mount. (default=%s) overrides 'BKP-Store':''' % (config.getStoreName('BKP'))) +parser.add_argument('-S', '--snapshot-mount-path', + dest='snpmount', + default=config.getMountPath('SNP'), + help='''set path to destination mountpoint of snapshot-mountpoint. (default=%s) overrides 'SNP-Path':''' % (config.getMountPath('SNP'))) +parser.add_argument('-s', '--snapshot-store', + dest='snpstore', + default=config.getStoreName('SNP'), + help='''set storename in snapshot-mount. (default=%s) overrides 'BKP-Store':''' % (config.getStoreName('SNP'))) +parser.add_argument('-N', '--notification', default=None, help='''Send notification. Possible values are "desktop" ''') +parser.add_argument('-U', '--notification_urgency', default=None, help='''Send notification. Possible values are 0=low, 1=normal, 2=critical ''') +#parser.set_defaults(func=main) + +subparsers = parser.add_subparsers() + +list_parser=subparsers.add_parser('list') +list_parser.add_argument("store", + default='SRC', + metavar='stores and snapshots', + nargs='*', + help="""one of SRC, BKP or SNP - where is the snapshot + located and one ore more snapshots""") +list_parser.add_argument('-r', '--reverse', action='store_true', default=False) +#list_parser.add_argument('--snap', action='store_true', default=False, help='list snapshots from the queried snapshot') +list_parser.add_argument('-p', '--parents', action='store_true', default=False, help="list parent snapshot from the queried snapshots") +list_parser.add_argument('-S', '--sisters', action='store_true', default=False, help="list snapshots from the queried snapshot with same parent (=sisters)") +list_parser.add_argument('-O', '--older-sisters', action='store_true', dest='older', default=False, help="list snapshots from the queried snapshot with same parent (=sisters) and smaller cgen") +list_parser.add_argument('-Y', '--younger-sisters', action='store_true', dest='younger', default=False, help="list snapshots from the queried snapshot with same parent (=sisters) and bigger cgen") +list_parser.add_argument('-c', '--children', dest='snap', action='store_true', default=False, help="""list snapshots from the queried snapshot""") +list_parser.add_argument('-f', '--fullpath', action='store_true', default=False, help="print full path from /") +list_parser.add_argument('-s', '--shortpath', action='store_true', default=False, help="print path including snapshotname") +list_parser.add_argument('-m', '--mountpath', action='store_true', default=False, help="print path relative to mountpoint of btrfs") +list_parser.add_argument('-i', '--info', dest='info', action='store_true', default=False, help="""Show infos on mkbackup""") +list_parser.add_argument('-P', '--show-StorePath', dest='showpathstore', action='store_true', default=False, help="""Print path of given stores""") +list_parser.add_argument('-t', '--show-tags', dest='showtags', action='store_true', default=False, help="""Print all available tags""") +list_parser.add_argument('-T', '--show-transfers', dest='showtransfers', action='store_true', default=False, help="""Print volumes to transfer by choosen tag""") +list_parser.add_argument('-d', '--show-database', dest='showdb', action='store_true', default=False, help="""Print whole snapshot-database""") +list_parser.add_argument('-y', '--show-youngest', action='store_true', dest='showyoungest', default=False, help="list youngest snapshots in store, with '-t tag' only for this tag") +list_parser.add_argument('--print-config', action='store_true', default=False, help="show the whole configuration or only for the given tag") +list_parser.add_argument('-o', '--outfile', dest='of', default=None, help="print configuration to this file, instead of stdout. Only in combination mit --print-config") +list_parser.add_argument('--all', dest='all', action='store_true', default=False, help="""show full list of all youngest snapshots in all intervals for all volumes (only in combination with -y""") +list_parser.add_argument('--tree', dest='tree', action='store_true', default=False, help="""Build tree from all snapshots (without subvolumes in it)""") +list_parser.set_defaults(func=lists) +list_parser.set_defaults(action='list') + +create_parser=subparsers.add_parser('create') +create_parser.add_argument("store", + default='SRC', + metavar='stores and snapshots', + nargs='*', + help="""one of SRC or SNP - where is the snapshot located and one ore more snapshots + which to be created in the same store. + BKP is ignored!!!""") +create_parser.add_argument('--no-clones', action='store_true', default=False, help="""do not use clones for transfer, only parent (if present)""") +create_parser.set_defaults(func=create) +create_parser.set_defaults(action='create') + +rollback_parser=subparsers.add_parser('rollback') +rollback_parser.add_argument("store", + default='SRC', + metavar='stores and snapshots', + nargs='*', + help="""one of SRC, BKP or SNP - where is the snapshot + located and one ore more snapshots""") +rollback_parser.add_argument('--dry-run', action='store_true', default=False, help="show only, do nothing") +rollback_parser.set_defaults(func=rollback) +rollback_parser.set_defaults(action='rollback') + +delete_parser=subparsers.add_parser('delete') +delete_parser.add_argument("store", + default='SRC', + metavar='stores and snapshots', + nargs='*', + help="""one of SRC, BKP or SNP - where is the snapshot + located and one ore more snapshots to be deleted""") +delete_parser.set_defaults(func=delete) +delete_parser.set_defaults(action='delete') + +transfer_parser=subparsers.add_parser('transfer') +transfer_parser.add_argument("snapshots", + default='SRC', + nargs='*', + help="""one of SRC or SNP - where is the snapshot located and one ore more snapshots + which to be transfered to the external backup-device. + BKP is ignored!!!""") +transfer_parser.add_argument('--no-clones', action='store_true', default=False, help="""do not use clones for transfer, only parent (if present)""") +transfer_parser.add_argument('-i', '--info', + dest='transferinfo', + action='store_true', + default=False, + help="""show only if initial or incremental transfer is done and the parents""") +transfer_parser.set_defaults(func=transfer) +transfer_parser.set_defaults(action='transfer') + +cleanup_parser=subparsers.add_parser('cleanup') +cleanup_parser.add_argument("snapshots", + nargs='*', + help='''cleanup all snapshots, which are older and more + than allowed in config''') +cleanup_parser.set_defaults(func=cleanup) +cleanup_parser.set_defaults(action='cleanup') + +setprop_parser=subparsers.add_parser('setprop') +setprop_parser.add_argument("store", + default='SRC', + metavar='stores and snapshots', + nargs='*', + help='''one of SRC, BKP or SNP - where is the snapshot + located and one ore more snapshots''') +setprop_parser.add_argument("-r", "--ro", + default=False, + action='store_true', + help='''setproperty to readonly. If -r is not given, + property is set to read-write''') +setprop_parser.set_defaults(func=setprop) +setprop_parser.set_defaults(action='setprop') + +restore_parser=subparsers.add_parser('restore') +restore_parser.add_argument("file", + nargs='*', + help="""restore all given files""") +restore_parser.add_argument("-n", "--no-preserve", + default=False, + action='store_true', + help="""if set, the original file will be overwritten. If not set, + the original file is renamed to filename.restore-${TIMESTAMP}""" ) +restore_parser.set_defaults(func=restore) +restore_parser.set_defaults(action='restore') + + + +if __name__ == '__main__': + args = parser.parse_args() + args.config = config + args.scriptname = os.path.basename(__file__) + stores = [] + snapshots = [] +# args.notification = dict() +# args.notification['bool']=False +# args.notification['type']=None + + # --- Logger --- + levels = (logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG, logging.NOTSET) + #print(logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG, logging.NOTSET) + print('verbose', args.verbose, levels[args.verbose]) + logger = logging.getLogger(args.scriptname) + logger.setLevel(logging.DEBUG) + # create formatter + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + # create file handler which logs even debug messages + # add formatter it to the handlers + if args.logfile is not None: + fh = logging.FileHandler('/var/log/'+args.logfile) + fh.setLevel(logging.DEBUG) + fh.setFormatter(formatter) + logger.addHandler(fh) + # create console handler with a higher log level + ch = logging.StreamHandler() + ch.setLevel(logging.ERROR) + # create formatter and add it to the handlers + #ch.setFormatter(formatter) + # add the handlers to logger + logger.addHandler(ch) + + # 'application' code + logger.debug('debug message') + logger.info('info message') + logger.warn('warn message') + logger.error('error message') + logger.critical('critical message') + + if not 'dry_run' in args: + args.dry_run=False + + if 'store' in args: + if isstring(args.store): args.store = args.store.split(' ') + else: + args.store = [] + + if 'snapshots' in args: + if isstring(args.snapshots): args.snapshots = args.snapshots.split() + else: + args.snapshots = [] + for i in args.store + args.snapshots: + if i == 'SNP' or i == 'SRC' or i == 'BKP': + stores.extend([i]) + else: + snapshots.extend([i.rstrip('/')]) + if len(stores) == 0: stores = ['SRC'] + +# if 'notify' in args: +# args.notification['bool'] = True if args.notify is True else False +# if 'notify_backend' in args: +# args.notification['type'] = args.notify_backend + + + + args.store = stores + args.snapshots = snapshots + + #print(args.store,args.snapshots) + + # set pathes in config to the given from cmdlin or default + args.config.setBKPPath(args.bkpmount) + args.config.setBKPStore(args.bkpstore) + args.config.setSNPPath(args.snpmount) + args.config.setSNPStore(args.snpstore) + + # tag = None is not allowed. Set it to misc, if not set + args.tagset = False + if args.tag == None: + args.tag = 'misc' + args.tagset = True # if no tag was given, and tag is set here, this value is true - it indicates, tag was set automatically to 'misc' + + + if args.systemvolumes: + args.snapshots=args.config.getVolumes(tag=args.tag) + if len(args.store) == 0: args.store=['SNP'] + + # set store and path to store depending on action + args.sourcepath, args.destpath = dict(), dict() + if hasattr(args,'action'): + if args.action == 'create' or args.action == 'transfer': + if 'BKP' in args.store: args.store.pop(args.store.index('BKP')) + args.destpath['BKP'] = args.config.getStorePath('BKP',args.tag) + if 'SRC' in args.store or 'SNP' not in args.store: + args.sourcepath['SRC'] = args.config.getStorePath('SRC',args.tag) + else: + args.sourcepath['SNP'] = args.config.getStorePath('SNP',args.tag) + else: + if len(args.store) == 0: + args.sourcepath['SRC'] = args.config.getStorePath('SRC',args.tag) + args.destpath['BKP'] = args.config.getStorePath('BKP',args.tag) + else: + for st in args.store: + args.sourcepath[st] = args.config.getStorePath(st,args.tag) + args.destpath[st] = args.config.getStorePath(st,args.tag) + logger.warn('''Action is %s + Source is %s + Destination is %s''' % (args.action,args.sourcepath,args.destpath)) + else: + logger.notset("""Version mkbackup_btrfs_config: %s +Version mkbackup-btrfs: %s +Author: %s""" % (confversion,__version__,__author__)) + quit() + +# DEBUG('SRC',args.sourcepath,level=5,verbose=args.verbose) +# DEBUG('DST',args.destpath,level=5,verbose=args.verbose) + logger.debug('SRC',args.sourcepath) + logger.debug('DST',args.destpath) + + # Set timestamp equal for all operations + args.ts = datetime.datetime.now() + args.timestamp = args.ts.strftime('%Y-%m-%d_%H:%M:%S') + #args.timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S') + + if hasattr(args, 'transferinfo'): + args.verbose += 1 + else: + args.transferinfo = False + + # set stdout and stderr for output of subprocess-calls for the verbose-levels + if args.verbose <= 1: + args.stdout=subprocess.DEVNULL + args.stderr=subprocess.DEVNULL + elif args.verbose <=2: + args.stdout=subprocess.DEVNULL + args.stderr=None + elif args.verbose > 2: + args.stdout=None + args.stderr=None + + if args.verbose > 0: + args.npb = False + + + #DEBUG("Arguments",args,verbose=args.verbose) + if args.verbose > 3: + DEBUG("Arguments",verbose=args.verbose) + for i in sorted(vars(args)): + DEBUG(i+': '+str(getattr(args,i,None)),verbose=args.verbose) + elif args.verbose > 1: + DEBUG("Arguments",args,verbose=args.verbose) + + args.mdbpart = len(args.snapshots ) * len(args.store) + args.mdb = EmDBUS() + args.mdb.reset(args.tag) + args.mdb.start(args.tag) + ################################################################# + ## Run action ## + ################################################################# + args.func(args) + + + # Check symlinks in stores, if they exist and ok or missing. + #for vol in Config().getVolumes(): + for vol in args.config.getVolumes(): + #print("VOL",vol) + for st in (args.sourcepath.keys()): + p = args.sourcepath[st] + #print("P",p,st) + #for sln in Config().ListSymlinkNames(): + for sln in args.config.ListSymlinkNames(): + link = p+'/'+vol+'.'+sln + #print("LINK",p,vol,sln,link) + #print("X",args.tag,st,args.config.ssh[args.tag][st]) + if Myos().path_exists(link,args.config.getssh(args.tag,st)): + #print("XX",args.tag,st) + if Myos().path_islink(link,args.config.getssh(args.tag,st)): + logger.debug("Symlink ok: %s/%s.%s" % (st,vol,sln)) + #DEBUG("Symlink ok: %s/%s.%s" % (st,vol,sln),level=4,verbose=args.verbose) + else: + #DEBUG("Symlink broken: %s/%s.%s --> remove it" % (st,vol,sln),level=4,verbose=args.verbose) + logger.debug("Symlink broken: %s/%s.%s --> remove it" % (st,vol,sln)) + Myos(dry=self.args.dry_run).remove(link,args.config.getssh(args.tag,st)) + else: + DEBUG("Symlink missing: %s/%s.%s" % (st,vol,sln),level=4,verbose=args.verbose) + for t in args.config.ssh_cons.keys(): + DEBUG("Close Connection to %s" % (t),level=3,verbose=args.verbose) + try: + connection = args.config.ssh_cons[t] + transport = connection.get_transport() + connection.close() + transport.send_ignore() + except: + # connection is closed + pass + DEBUG(' -> closed' if args.config.ssh_cons[t].get_transport() == None else 'not closed', level=3,verbose=args.verbose) + #args.config.ssh_cons[t].close() +# for s in args.config.ssh[t].keys(): +# if not args.config.ssh[t][s]['ssh'] == None: +# print("CLOSE",t,s) +# args.config.ssh[t][s]['ssh'].close() + + # print summary over all actions + volumes = list() + + if not args.action == 'list' and not args.action == 'restore': + DEBUG("---=== SUMMARY ===---",level=0,verbose=args.verbose) + DEBUG("""Stores: %s +snapshots: %s + """ % (args.store,args.snapshots)) + DEBUG("Created Subvolumes:",level=0,verbose=args.verbose) + volumes.append('Volumes created:') + if BtrfsListing.CreatedSubvolumes.is_empty(): + volumes.append('---') + print('''--- + ''') + while not BtrfsListing.CreatedSubvolumes.is_empty(): + i = BtrfsListing.CreatedSubvolumes.pop() + volumes.append(i[1]) + print(i) + volumes.append('') + + DEBUG("Transfered Subvolumes:",level=0,verbose=args.verbose) + volumes.append('Volumes transfered:') + if BtrfsListing.TransferedSubvolumes.is_empty(): + volumes.append('---') + print('''--- + ''') + while not BtrfsListing.TransferedSubvolumes.is_empty(): + i = BtrfsListing.TransferedSubvolumes.pop() + volumes.append(i[1]) + print(i) + volumes.append('') + + DEBUG("Deleted Subvolumes:",level=0,verbose=args.verbose) + volumes.append('Volumes deleted:') + if BtrfsListing.DeletedSubvolumes.is_empty(): + volumes.append('---') + print(''' --- + ''') + while not BtrfsListing.DeletedSubvolumes.is_empty(): + i = BtrfsListing.DeletedSubvolumes.pop() + volumes.append(i[1]) + print(i) + volumes.append('') + + + print(args.notification, config.getNotification(intv=args.tag), args.notification_urgency, config.getUrgency(intv=args.tag)) + if args.notification == None: + args.notification = config.getNotification(intv=args.tag) + if args.notification_urgency == None: + args.notification_urgency = config.getUrgency(intv=args.tag) + print(args.notification, args.notification_urgency) + + if args.notification == 'desktop': +# notify = desktop_notification(args, urgency=args.notification_urgency) +# notify.send_signal(args, *volumes) +# print(volumes) +# print(*volumes) + + msg = dict() + msg['sender'] = "mkbackup" + msg['header'] = "%s-backup" % (args.tag) + msg['body'] = """am %s +um %s Uhr abgeschlossen. + +%s +http://www.google.com +TEST +(Ugency: normal) +""" % (args.ts.strftime('%Y-%m-%d'), args.ts.strftime('%H:%M:%S'), '\r'.join(volumes)) +# msg['action'] = dict() +# msg['action']['action1'] = dict() +# msg['action']['action1']['title'] = 'Open Backup' +# msg['action']['action1']['loc'] = 'file:~/backup' + + + advnotify = Notification() + advnotify.normal(msg) + elif args.notification == None: + logger.critical("No notification at all") + else: + logger.critical("No notification at all") + + + logger.critical("---== (%s) finnished %s %s at %s ==---" % (os.getpid(),args.func.__name__, args.tag, args.timestamp)) + + args.mdb.finished(args.tag) + +#import ntfy + +# import smtplib +# server = smtplib.SMTP('localhost', 587) +# +# #Next, log in to the server +# server.login("username", "verysecret") +# +# #Send the mail +# msg = """ +# Hello!""" # The /n separates the message from the headers +# server.sendmail("first.recipient@provider1.example", "second.recipient@provider2.example", msg) +# diff --git a/files/usr/local/bin/syssubvol.old b/files/usr/local/bin/syssubvol.old new file mode 100755 index 0000000..4058060 --- /dev/null +++ b/files/usr/local/bin/syssubvol.old @@ -0,0 +1,15 @@ +#!/bin/bash +#mawk '$5 ~ "^/$" {gsub ("/","",$4);print $4}' /proc/self/mountinfo + +#SV1=$(basename "$(grub-mkrelpath /)") +SV1="XXX" + +[ -x /bin/cat ] || exit 1 +SV2=$(grep -sq "=subvol=" /proc/cmdline && /bin/cat /proc/cmdline |sed 's/^.*=subvol=\([^ ]*\) .*$/\1/') + +#echo "SV1: $SV1 SV2: $SV2" +if [ "$SV1" == "$SV2" ];then + echo "$SV1" +else + echo "$SV2" +fi diff --git a/files/usr/share/doc/mkbackup-btrfs/fstab.mkbackup.example b/files/usr/share/doc/mkbackup-btrfs/fstab.mkbackup.example new file mode 100755 index 0000000..987cb41 --- /dev/null +++ b/files/usr/share/doc/mkbackup-btrfs/fstab.mkbackup.example @@ -0,0 +1,27 @@ +##################################### +## System - Lokal +##################################### +# Hier kein subvol angeben. Ist in grub.cfg "rootflags=subvol=@debian" +LABEL=debian / btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime 0 0 +#LABEL=debian /boot/grub/x86_64-efi btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/boot-grub-x86_64-efi 0 0 +LABEL=debian /home btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/home 0 0 +LABEL=debian /usr/local btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/usr-local 0 0 +LABEL=debian /opt btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/opt 0 0 +LABEL=debian /var/opt btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-opt 0 0 +LABEL=debian /var/log btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-log 0 0 +LABEL=debian /var/lib/mpd btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-lib-mpd 0 0 +#LABEL=debian /var/lib/named btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-lib-named 0 0 +LABEL=debian /var/spool btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-spool 0 0 +LABEL=debian /var/spool/dovecot btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-spool-dovecot 0 0 +LABEL=debian /var/tmp btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-tmp 0 0 +#LABEL=debian /var/www btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-www 0 0 +LABEL=debian /var/cache btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/var-cache 0 0 +#LABEL=debian /srv btrfs defaults,compress=lzo,nospace_cache,autodefrag,noinode_cache,noatime,subvol=__ALWAYSCURRENT__/srv 0 0 +tmpfs /tmp tmpfs nosuid,size=25% 0 0 + + +UUID=2AE9-56D2 /boot/efi vfat umask=0077 0 0 +#################################### +## BACKUP +#################################### +## Ist über unit-Files in /etc/systemd/system geregelt diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/convenience.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/convenience.js new file mode 100644 index 0000000..7cbde09 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/convenience.js @@ -0,0 +1,92 @@ +/* + Copyright (c) 2011-2012, Giovanni Campagna + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of the GNOME nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + 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 THE COPYRIGHT HOLDER 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. +*/ + +var Gettext = imports.gettext; +var Gio = imports.gi.Gio; + +var Config = imports.misc.config; +var ExtensionUtils = imports.misc.extensionUtils; + +/** + * initTranslations: + * @domain: (optional): the gettext domain to use + * + * Initialize Gettext to load translations from extensionsdir/locale. + * If @domain is not provided, it will be taken from metadata['gettext-domain'] + */ +function initTranslations(domain) { + var extension = ExtensionUtils.getCurrentExtension(); + + domain = domain || extension.metadata['gettext-domain']; + + // check if this extension was built with "make zip-file", and thus + // has the locale files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell + var localeDir = extension.dir.get_child('locale'); + if (localeDir.query_exists(null)) + Gettext.bindtextdomain(domain, localeDir.get_path()); + else + Gettext.bindtextdomain(domain, Config.LOCALEDIR); +} + +/** + * getSettings: + * @schema: (optional): the GSettings schema id + * + * Builds and return a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is not provided, it is taken from + * metadata['settings-schema']. + */ +function getSettings(schema) { + var extension = ExtensionUtils.getCurrentExtension(); + + schema = schema || extension.metadata['settings-schema']; + + var GioSSS = Gio.SettingsSchemaSource; + + // check if this extension was built with "make zip-file", and thus + // has the schema files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell (and therefore schemas are available + // in the standard folders) + var schemaDir = extension.dir.get_child('schemas'); + var schemaSource; + if (schemaDir.query_exists(null)) + schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), + GioSSS.get_default(), + false); + else + schemaSource = GioSSS.get_default(); + + var schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) + throw new Error('Schema ' + schema + ' could not be found for extension ' + + extension.metadata.uuid + '. Please check your installation.'); + + return new Gio.Settings({ settings_schema: schemaObj }); +} + diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/extension.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/extension.js new file mode 100644 index 0000000..e95911c --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/extension.js @@ -0,0 +1,638 @@ +var GLib = imports.gi.GLib; +var Lang = imports.lang; +var Main = imports.ui.main; +var PanelMenu = imports.ui.panelMenu; +var PopupMenu = imports.ui.popupMenu; + +var St = imports.gi.St; +var Shell = imports.gi.Shell; + +var Gettext = imports.gettext.domain('gnome-shell-extensions'); +var _ = Gettext.gettext; + +var ExtensionUtils = imports.misc.extensionUtils; +var Me = ExtensionUtils.getCurrentExtension(); +var Convenience = Me.imports.convenience; +var Util = imports.misc.util; +var PopupServiceItem = Me.imports.popupServiceItem.PopupServiceItem; +var PopupTargetItem = Me.imports.popupTargetItem.PopupTargetItem; +var PopupMenuItem = Me.imports.popupManuallyItem.PopupServiceItem; +var MountMenuItem = Me.imports.popupMountItem.MountMenuItem; +var DriveMenuItem = Me.imports.popupDriveItem.DriveMenuItem; +var VolMenuItem = Me.imports.popupBkpVolumItem.PopupBKPItem; +var Gio = imports.gi.Gio; +var Mainloop = imports.mainloop; + +var refreshTime = 3.0; +var MainLabel; +var icon; +var MainIcon; +var ExtIcon; +var extMediaName = 'external backup-drive'; +var Drives = new Object(); + +//var DisabledIcon = 'my-caffeine-off-symbolic'; +//var EnabledIcon = 'my-caffeine-on-symbolic'; +var DisabledIcon = 'system-run-symbolic'; +var EnabledIcon = 'system-run-symbolic'; +//var ConfFile = '/etc/mkbackup-btrfs.conf'; +var ConfFile = '/tmp/mkbackup-btrfs.conf.tmp'; + + +var BackupManager = new Lang.Class({ + Name: 'BackupManager', + Extends: PanelMenu.Button, + + _entries: [], + + _init: function() { + + this._drives = [ ]; + this._volumes = [ ]; + this._mounts = [ ]; + + //Set a FileMonitor on the config-File. So the Config-File is only + //read, when it changed. + this.GF = Gio.File.new_for_path(ConfFile); + //this._monitorConf = this.GF.monitor_file(Gio.FileMonitorFlags.NONE,null,null,null) + this._monitorConf = this.GF.monitor_file(Gio.FileMonitorFlags.NONE,null) + this._monitorConf.connect("changed", Lang.bind(this, function(monitor, file, o, event) { + // without this test, _loadConfig() is called more than once!! + if (event == Gio.FileMonitorEvent.CHANGES_DONE_HINT && ! /~$/.test(file.get_basename())) { + this._loadConfig(); + } + })); + + this._loadConfig(); + this.watchvolume = this._run_command('systemd-escape --path '+this.bkpmnt)+'.mount' + + PanelMenu.Button.prototype._init.call(this, 0.0); + + var hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' }); + MainIcon = new St.Icon({icon_name: 'drive-harddisk-usb-symbolic', + style_class: 'system-status-icon'}); + ExtIcon = new St.Icon({icon_name: 'drive-harddisk-usb-symbolic', + style_class: 'system-status-icon'}); + ExtIcon.hide(); + + MainLabel = new St.Label({ text: '---', + }); + + hbox.add_child(MainLabel); + hbox.add_child(MainIcon); + hbox.add_child(ExtIcon); + hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM)); + + this.actor.add_actor(hbox); + this.actor.add_style_class_name('panel-status-button'); + this.actor.connect('button-press-event', Lang.bind(this, function() { + this._refresh(); + })); + + Main.panel.addToStatusArea('backupManager', this); + + //this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // Fill the Menu + // First a Entry to open backup-Location + var bkpitem = this.menu.addAction(_("Open Backups"), function(event) { + var context = global.create_app_launch_context(event.get_time(), -1); + var GF = Gio.File.new_for_path('backup'); + Gio.AppInfo.launch_default_for_uri(GF.get_uri(),context); + }); + + this.extItem = new PopupTargetItem(extMediaName, this._check_service('mkbackup@BKP.target','active')); + this.extItem.connect('toggled', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand('mkbackup@BKP.target', + (this._check_service('mkbackup@BKP.target','active') ? 'stop' : 'start'), + 'system')); + })); + this.menu.addMenuItem(this.extItem); + + this.snapitem = new PopupMenu.PopupMenuItem(_("Take snapshot now (tag: manually)")); + this.snapitem.connect('activate', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand('mkbackup@manually.service', 'restart', 'system')); + this.menu.close(); + })); + this.menu.addMenuItem(this.snapitem); + + this.bkpsubmenu = new PopupMenu.PopupSubMenuMenuItem(_("Backup-Intervalle"), true); + this.bkpsubmenu.icon.icon_name = 'system-run-symbolic'; + this.menu.addMenuItem(this.bkpsubmenu); + + //this.descrsubmenu = new PopupMenu.PopupSubMenuMenuItem(_("Info"), true); + //this.descrsubmenu.icon.icon_name = 'system-run-symbolic'; + //this.bkpsubmenu.addMenuItem(this.descrsubmenu); + + if(this._entries.length > 0) + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + this._refreshID = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._refresh_panel)); + // New Volumes to use as backup-media + this._monitor = Gio.VolumeMonitor.get(); + + this._addedDriveId = this._monitor.connect('drive-connected', Lang.bind(this, function(monitor, drive) { + log('DRIVE CONNECTED',drive.get_name()); + this._DriveAdded(drive); + })); + + this._removedDriveId = this._monitor.connect('drive-disconnected', Lang.bind(this, function(monitor, drive) { + log('DRIVE DISCONNECTED',drive.get_name(),this._removedDriveId) + this._DriveRemoved(drive); + //this.extItem.label.text = _("external backup-drive"); + //this.drvsubmenu.destroy(); + })); + + this._addedVolumeId = this._monitor.connect('volume-added', Lang.bind(this, function(monitor, volume){ + this._addVolume(volume); + this._VolumeAdded(volume); + log('VOLUMES',JSON.stringify(this._drives)) + })) + + this._removedVolumeId = this._monitor.connect('volume-removed', Lang.bind(this, function(monitor, volume){ + //this._VolumeRemoved(volume); + })) + + /*this._monitor.get_volumes().forEach(Lang.bind(this, function(volume) { + this._VolumeAdded(volume); + })); + + + this._addedMountId = this._monitor.connect('mount-added', Lang.bind(this, function(monitor, mount) { + log('MOUNT CONNECTED',mount.get_name()) + log(mount.get_name()) + var volume = mount.get_volume() + log(volume.get_name(),volume.get_uuid()) + //log(mount.get_name()) + //this._showDrive(drive); + }));*/ + + //log(Gio.UnixMountPoint.get_device_path('/var/cache/backup')) + return; + }, + + _DriveAdded: function(drive) { + var Dident = drive.enumerate_identifiers(); + var u_dev = drive.get_identifier('unix-device'); + var d_name = drive.get_name(); + //log('DRIVE unix-device',u_dev,d_name) + this._drives[d_name] = new Object() + this._drives[d_name]['drive'] = drive; + this._drives[d_name]['device'] = drive.get_identifier('unix-device'); + //this._drives[d_name]['uuid'] = drive.get_identifier('uuid'); + this._drives[d_name]['volumes'] = new Object() + log("ABCDE",this._drives[d_name]['device']); + /*if (drive.has_volumes()) { + log('DHV',drive.get_volumes()); + var VList = drive.get_volumes(); + VList.forEach(Lang.bind(this, function(volume){ + var v_name = volume.get_name(); + log(v_name) + this._drives[d_name]['volumes'][v_name] = this._addVolume(volume); + })); + //VList.free() + } else { + log('DHNV',drive.get_volumes()); + }; + log('VOLUMES',JSON.stringify(this._drives)) + log('X',this._drives['ST1000LM024 HN-M101MBB']['device']) + */ + /* + this.drvsubmenu = new PopupMenu.PopupSubMenuMenuItem(drive.get_name(), true); + this.drvsubmenu.icon.icon_name = 'drive-harddisk-usb-symbolic'; + this.menu.addMenuItem(this.drvsubmenu); + */ + //log('Drive added',drive.get_name()) + //this.drvsubmenu = new PopupMenu.PopupSubMenuMenuItem(_(drive.get_name()), true); + //this.drvsubmenu.icon.icon_name = 'drive-harddisk-usb-symbolic'; + //this.drvsubmenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + }, + + _addVolume: function(volume) { + var drives = volume.get_drive(); + //log('D',drives.get_name()) + //log('V',volume.get_name()) + //this._drives[drives.get_name()][volume.get_name()] = volume; + volume.enumerate_identifiers(); + var vol = new Object() + //log(volume.get_identifier('uuid')) + //log(volume.get_identifier('unix-device')) + //log(volume.get_identifier('class')) + //log(volume.get_identifier('label')) + //vol['volume'] = volume; + //vol['uuid'] = volume.get_identifier('uuid'); + //vol['u_device'] = volume.get_identifier('unix-device'); + //vol['vclass'] = volume.get_identifier('class'); + //vol['label'] = volume.get_identifier('label'); + //this._drives[drives.get_name()]['volumes'][volume.get_name()] = v_name; + return vol + }, + + _DriveRemoved: function(drive) { + var me = this.menu._getMenuItems(); + this._removeItemByLabel(this.menu, drive.get_name()); + + + //this.drvsubmenu.destroy(); + }, + + _VolumeRemoved: function(volume) { + //Volume is removed, after it's mounted from system(d) + log('VOLUME REMOVED',volume.get_name()) + }, + + _VolumeAdded: function(volume) { + //Volume is added, after it's unmounted from system(d) + log('VOLUME CONNECTED',volume.get_name()) + log('ID',this._addedVolumeId) + if (volume.get_drive() == null) + return + var drive = volume.get_drive() + if ( !volume.can_mount() || !drive.is_removable()) + return + log(volume.get_mount()) + log(volume.enumerate_identifiers()) + log(volume.get_identifier('uuid')) + log(volume.get_identifier('unix-device')) + log(volume.get_identifier('class')) + log(volume.get_identifier('label')) + var dr = volume.get_drive() + this._drives[dr.get_name()] = dr + log(dr.get_name()) + log(dr.enumerate_identifiers()) + log(dr.get_identifier('unix-device')) + + var me + try { + me = this.drvsubmenu.menu._getMenuItems(); + } catch(e) { + this._DriveAdded(drive) + } + //me = this.drvsubmenu.menu._getMenuItems(); + + var menuItem = new VolMenuItem(volume, false); + + var VF = Gio.File.new_for_path('/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules'); + if (VF.query_exists(null)) { + log('UDEV exists','/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules') + menuItem.setToggleState(true); + } else { + log('UDEV not exists','/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules') + menuItem.setToggleState(false); + } + + + //this._removeItemByLabel(this.drvsubmenu.menu, volume.get_name()); + //this.drvsubmenu.menu.addMenuItem(menuItem); + var connID = menuItem.connect('toggled', Lang.bind(this, function() { + log('ACTIVE?',menuItem.state,menuItem.label.text) + if (menuItem.state) { + reg = 'register' + } else { + reg = 'unregister' + } + GLib.spawn_command_line_async( + this._getCommand('mkbackup-'+reg+'@'+volume.get_identifier('unix-device')+'.service', 'start', 'system')); + })); + log(connID,volume.get_name()) + //this.menu.addMenuItem(this.drvsubmenu,1); + }, + + _removeItemByLabel: function(menu, label) { + log('LAB',label) + var children = menu._getMenuItems(); + for (var i = 0; i < children.length; i++) { + var item = children[i]; + log('REM',item.label.text,label) + if (item.label.text == label) + log('DESTROY',item.label.text) + //item.destroy(); + } + }, + + + + _addMount: function(mount) { + var item = new MountMenuItem(mount); + this._mounts.unshift(item); + this.menu.addMenuItem(item, 0); + }, + + _removeMount: function(mount) { + for (var i = 0; i < this._mounts.length; i++) { + var item = this._mounts[i]; + if (item.mount == mount) { + item.destroy(); + this._mounts.splice(i, 1); + return; + } + } + log ('Removing a mount that was never added to the menu'); + }, + + _run_command: function(COMMAND) { + var output = ""; + try { + //output = GLib.spawn_command_line_sync(COMMAND, null, null, null, null); + output = GLib.spawn_command_line_sync(COMMAND); + } catch(e) { + throw e; + } + + return output[1].toString().replace(/\n$/, "") + ""; + }, + + + + _showVolume: function(volume) { + var drive = volume.get_drive(); + var mount = volume.get_mount(); + if (drive != null && drive.is_removable()){ + log('SVDRIVE',drive.get_name(),drive.is_removable()) + log('SVVOL',volume.get_name(),volume.get_uuid(),drive.is_removable()); + } + }, + + _showDrive: function(drive) { + //extMediaName = 'external backup-drive'; + log('SDDRIVE',drive.get_name(),drive.get_volumes(),drive.has_media(),drive.is_removable(),drive.has_volumes(),drive.enumerate_identifiers()); + if (drive.is_removable() && drive.has_volumes()) { + extMediaName = drive.get_name(); + log('SDDREMOV') + + drive.get_volumes().forEach(Lang.bind(this, function(volume) { + if (volume.can_mount()){ + log('SDVOL',volume.get_name()); + if ( volume.get_mount() != null ) { + var mount = volume.get_mount(); + log('SDMR',mount.get_root()); + } + } + })); + } else { + log(drive.is_removable(),drive.has_volumes(),drive.get_volumes()); + } + }, + + _checkMount: function(mount) { + log('CHECK MOUNT '+mount.can_unmount()+' '+mount.can_eject()); + log('VOLUME '+mount.get_volume()); + return(mount.can_unmount() || mount.can_eject()) + }, + + _getCommand: function(service, action, type) { + var command = "systemctl" + + command += " " + action + command += " " + service + command += " --" + type + if (type == "system" && (action != 'is-active' && action != 'is-enabled')) + command = "pkexec --user root " + command + + return 'sh -c "' + command + '; exit;"' + }, + + _getDriveMounted: function(udevice) { + command = "/bin/grep" + command += " " + udevice + command += "/proc/mounts" + return 'sh -c "' + command + '; exit:"' + }, + + _refresh_panel : function() { + + //log('YY',this._drives['ST1000LM024 HN-M101MBB']['volumes']) + //log('YY',this._drives['ST1000LM024 HN-M101MBB']) + var active = false; + var volumes = [] + var mounted = false; + volumes.push(this.watchvolume) + + // TODO: find all Volumes on the drive, holding the backup and add this + // volumes to the list + volumes.push('home-jakob-Videos-extern.mount') + volumes.push('home-media.mount') + //for (var d in this._drives['ST1000LM024 HN-M101MBB']) { + /*for (var d in this._drives) { + log("D",d,this._drives[d].get_name()); + };*/ + + this.aout = GLib.spawn_command_line_sync( + this._getCommand(this.services.join(' '), 'is-active', 'system'))[1].toString().split('\n'); + //log(this.aout.indexOf('active')); + + var apos = this.aout.indexOf('active') + active = this.aout.indexOf('active') >= 0 + + var vout = GLib.spawn_command_line_sync( + this._getCommand(volumes.join(' '), 'is-active', 'system'))[1].toString().split('\n'); + mounted = vout.indexOf('active') >= 0 + + this.bkpsubmenu.icon.style = (active ? "color: #ff0000;" : "color: revert;"); + //this.bkpsubmenu.icon.style_class = (active ? "system-status-icon" : "system-status-icon-red"); + MainIcon.style = (mounted ? "color: #ff0000;" : "color: revert;"); + ExtIcon.style = (mounted ? "color: #ff0000;" : "color: revert;"); + (mounted ? ExtIcon.show() : ExtIcon.hide()); + + //ExtIcon.actor = (mounted ? "visibile = true;" : "visible = false;"); + var mlabel = (mounted ? _("mounted") : ""); + var alabel = (active ? this.services[apos] : ""); + MainLabel.set_text(mlabel + ' ' + alabel); + //MainLabel.set_text(mounted ? _("mounted") : ""); + //MainLabel.set_text(active ? this.services[apos] : ""); + + if (this.menu.isOpen) { + //Menu is open + var me = this.menu._getMenuItems(); + me.forEach(Lang.bind(this, function(item) { + //log(item.label.text) + if ( item.label.text == extMediaName ) { + item.setToggleState(this._check_service('mkbackup@BKP.target','active')); + } + })); + if (this.bkpsubmenu.menu.isOpen) { + //Submenu Backu-intervals open + this._refresh(); + }; + this._refresh(); + } + + if (this._refreshID != 0) + Mainloop.source_remove(this._refreshID); + this._refreshID = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._refresh_panel)); + GLib.Source.set_name_by_id(this._refreshID, '[gnome-shell] this._refresh_panel'); + return false; + }, + + _check_service: function(service,stat) { + var [_, aout, aerr, astat] = GLib.spawn_command_line_sync( + this._getCommand(service, 'is-'+stat, 'system')); + return (astat == 0); + }, + + _refresh: function() { + var me = this.bkpsubmenu.menu._getMenuItems(); + //log(this.services.join(' ')) + + var eout = GLib.spawn_command_line_sync( + this._getCommand(this.services.join(' '), 'is-enabled', 'system'))[1].toString().split('\n'); + //log(eout); + + this.aout = GLib.spawn_command_line_sync( + this._getCommand(this.services.join(' '), 'is-active', 'system'))[1].toString().split('\n'); + + this._entries.forEach(Lang.bind(this, function(service,index,arr) { + if (! arr[index].enabled == (eout[index] == 'enabled')) + arr[index].changed = true + arr[index].enabled = (eout[index] == 'enabled' ? true : false) + + if (! arr[index].active == (this.aout[index] == 'active')) + arr[index].changed = true + arr[index].active = (this.aout[index] == 'active' ? true : false) + })); + + this._entries.forEach(Lang.bind(this, function(service,index,arr) { + var serviceItem + me.forEach(Lang.bind(this, function(item) { + if ( item.label.text == service['descr']+' ('+service['name'] + ')' ) { + arr[index].found = true; + serviceItem = item; + me.splice(me.indexOf(item),1); + } + })); + + if ( arr[index].changed ) { + if (arr[index].found) { + if ( service.staticserv ) { + serviceItem.setToggleState(service.active); + } else { + serviceItem.setToggleState(service.enabled); + } + } else { + if ( service.staticserv ) { + serviceItem = new PopupTargetItem(service['descr']+' ('+service['name'] + ')', service.active); + this.bkpsubmenu.menu.addMenuItem(serviceItem); + + serviceItem.connect('toggled', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand(service['service'], (this._check_service(service.service, 'active') ? 'stop' : 'start'), service["type"])); + })); + } else { + serviceItem = new PopupServiceItem(service['descr']+' ('+service['name'] + ')', service.enabled); + this.bkpsubmenu.menu.addMenuItem(serviceItem); + + serviceItem.connect('toggled', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand(service['service'], (this._check_service(service.service, 'enabled') ? 'disable' : 'enable'), service["type"])); + })); + + serviceItem.actionButton.connect('clicked', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand(service['service'], (this._check_service(service.service, 'active') ? 'stop' : 'start'), service["type"])); + this.menu.close(); + })); + } + } + if (serviceItem.actionButton.child) { + serviceItem.actionButton.child.icon_name = (service.active ? EnabledIcon : DisabledIcon); + serviceItem.actionButton.child.style = (service.active ? "color: #ff0000;" : "color: revert;"); + }; + if (serviceItem.transferButton) { + serviceItem.transferButton.style = (service.tr ? "text-decoration: revert;" : "text-decoration: line-through;"); + }; + if (serviceItem.descriptionLabel) { + serviceItem.descriptionLabel.label = service.descr; + }; + + + //log('Changed',service.service) + } + //log('X',arr[index].service,arr[index].enabled,arr[index].active,arr[index].changed) + arr[index].changed = false; + })); + + this.bkpsubmenu.menu._getMenuItems().forEach(Lang.bind(this, function(item) { + var mic = 0 + if ( me.length > mic ) { + for (var i = mic; i < me.length; i++) { + if (item == me[i]) { + log('DESTROY',me[i].label.text); + item.destroy(); + } + } + } + })); + + return true; + }, + + _loadConfig: function() { + //log('LOAD CONFIG') + var intervals + this.services = [] + var kf = new GLib.KeyFile() + var obj = new Object(); + this._entries = []; + + if(kf.load_from_file(ConfFile,GLib.KeyFileFlags.NONE)){ + //intervals = kf.get_groups()[0]; + this.bkpmnt = kf.get_value('DEFAULT','bkpmnt') + + //log('BKP',this.bkppath) + kf.get_groups()[0].forEach(Lang.bind(this, function(interval) { + var obj = new Object(); + var i = "" + if (interval === 'DEFAULT') + i = 'misc' + else + i = interval + obj.name = i +' backups'; + obj.interval = interval; + obj.service = "mkbackup@"+i+".service"; + this.services.push(obj.service) + obj.type = "system"; + obj.enabled = false; + obj.active = false; + obj.changed = true; + obj.found = false; + obj.staticserv = false; + + try { + obj.tr = (kf.get_value(interval,"transfer").toLowerCase() === "true"); + } catch(err) { + obj.tr = (kf.get_value("DEFAULT","transfer").toLowerCase() === "true"); + } + + try { + obj.descr = (kf.get_value(interval,"description")); + } catch(err) { + try { + obj.descr = (kf.get_value("DEFAULT","description")); + } catch(err) { + obj.descr = ""; + } + } + this._entries.push(obj)})); + } + } +}); + +var backupManager; + +function init(extensionMeta) { + //Convenience.initTranslations(); + var theme = imports.gi.Gtk.IconTheme.get_default(); + theme.append_search_path(extensionMeta.path + "/icons"); +} + + +function enable() { + backupManager = new BackupManager(); + +} + +function disable() { + backupManager.destroy(); +} diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/extension.js.new b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/extension.js.new new file mode 100644 index 0000000..a2466df --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/extension.js.new @@ -0,0 +1,576 @@ +var GLib = imports.gi.GLib; +var Lang = imports.lang; +var Main = imports.ui.main; +var PanelMenu = imports.ui.panelMenu; +var PopupMenu = imports.ui.popupMenu; + +var St = imports.gi.St; +var Shell = imports.gi.Shell; + +var Gettext = imports.gettext.domain('gnome-shell-extensions'); +var _ = Gettext.gettext; + +var ExtensionUtils = imports.misc.extensionUtils; +var Me = ExtensionUtils.getCurrentExtension(); +var Convenience = Me.imports.convenience; +var Util = imports.misc.util; +var PopupServiceItem = Me.imports.popupServiceItem.PopupServiceItem; +var PopupTargetItem = Me.imports.popupTargetItem.PopupTargetItem; +var PopupMenuItem = Me.imports.popupManuallyItem.PopupServiceItem; +var MountMenuItem = Me.imports.popupMountItem.MountMenuItem; +var DriveMenuItem = Me.imports.popupDriveItem.DriveMenuItem; +var VolMenuItem = Me.imports.popupBkpVolumItem.PopupBKPItem; +var Gio = imports.gi.Gio; +var Mainloop = imports.mainloop; + +var refreshTime = 3.0; +var MainLabel; +var icon; +var MainIcon; +var extMediaName = 'external backup-drive'; +var Drives = new Object(); + +//var DisabledIcon = 'my-caffeine-off-symbolic'; +//var EnabledIcon = 'my-caffeine-on-symbolic'; +var DisabledIcon = 'system-run-symbolic'; +var EnabledIcon = 'system-run-symbolic'; + + +var BackupManager = new Lang.Class({ + Name: 'BackupManager', + Extends: PanelMenu.Button, + + _entries: [], + + _init: function() { + + this._drives = [ ]; + this._volumes = [ ]; + this._mounts = [ ]; + + //Set a FileMonitor on the config-File. So the Config-File is only + //read, when it changed. + this.GF = Gio.File.new_for_path('/etc/mksnapshot.conf'); + this._monitorConf = this.GF.monitor_file(Gio.FileMonitorFlags.NONE,null,null,null) + this._monitorConf.connect("changed", Lang.bind(this, function(monitor, file, o, event) { + // without this test, _loadConfig() is called more than once!! + if (event == Gio.FileMonitorEvent.CHANGES_DONE_HINT && ! /~$/.test(file.get_basename())) { + this._loadConfig(); + } + })); + + this._loadConfig(); + this.watchvolume = this._run_command('systemd-escape --path '+this.bkpmnt)+'.mount' + + PanelMenu.Button.prototype._init.call(this, 0.0); + + var hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' }); + MainIcon = new St.Icon({icon_name: 'drive-harddisk-usb-symbolic', + style_class: 'system-status-icon'}); + + MainLabel = new St.Label({ text: '---', + }); + + hbox.add_child(MainLabel); + hbox.add_child(MainIcon); + hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM)); + + this.actor.add_actor(hbox); + this.actor.add_style_class_name('panel-status-button'); + this.actor.connect('button-press-event', Lang.bind(this, function() { + this._refresh(); + })); + + Main.panel.addToStatusArea('backupManager', this); + + //this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // Fill the Menu + // First a Entry to open backup-Location + var bkpitem = this.menu.addAction(_("Open Backups"), function(event) { + var context = global.create_app_launch_context(event.get_time(), -1); + var GF = Gio.File.new_for_path('backup'); + Gio.AppInfo.launch_default_for_uri(GF.get_uri(),context); + }); + + this.extItem = new PopupTargetItem(extMediaName, this._check_service('mkbackup@BKP.target','active')); + this.extItem.connect('toggled', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand('mkbackup@BKP.target', + (this._check_service('mkbackup@BKP.target','active') ? 'stop' : 'start'), + 'system')); + })); + this.menu.addMenuItem(this.extItem); + + this.snapitem = new PopupMenu.PopupMenuItem(_("Take snapshot now")); + this.snapitem.connect('activate', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand('mkbackup@manually.service', 'restart', 'system')); + this.menu.close(); + })); + this.menu.addMenuItem(this.snapitem); + + this.bkpsubmenu = new PopupMenu.PopupSubMenuMenuItem(_("Backup-Intervalle"), true); + this.bkpsubmenu.icon.icon_name = 'system-run-symbolic'; + this.menu.addMenuItem(this.bkpsubmenu); + + if(this._entries.length > 0) + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + this._refreshID = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._refresh_panel)); + // New Volumes to use as backup-media + this._monitor = Gio.VolumeMonitor.get(); + + this._removedDriveId = this._monitor.connect('drive-disconnected', Lang.bind(this, function(monitor, drive) { + log('DRIVE DISCONNECTED',drive.get_name(),this._removedDriveId) + this._DriveRemoved(drive); + //this.extItem.label.text = _("external backup-drive"); + //this.drvsubmenu.destroy(); + })); + + this._addedDriveId = this._monitor.connect('drive-connected', Lang.bind(this, function(monitor, drive) { + log('DRIVE CONNECTED',drive.get_name()); + this._DriveAdded(drive); + })); + + this._addedVolumeId = this._monitor.connect('volume-added', Lang.bind(this, function(monitor, volume){ + this._VolumeAdded(volume); + log('VOLUMES',JSON.stringify(this._drives)) + })) + + this._removedVolumeId = this._monitor.connect('volume-removed', Lang.bind(this, function(monitor, volume){ + this._VolumeRemoved(volume); + })) + + /*this._monitor.get_volumes().forEach(Lang.bind(this, function(volume) { + this._VolumeAdded(volume); + })); + + + this._addedMountId = this._monitor.connect('mount-added', Lang.bind(this, function(monitor, mount) { + log('MOUNT CONNECTED',mount.get_name()) + log(mount.get_name()) + var volume = mount.get_volume() + log(volume.get_name(),volume.get_uuid()) + //log(mount.get_name()) + //this._showDrive(drive); + }));*/ + + //log(Gio.UnixMountPoint.get_device_path('/var/cache/backup')) + return; + }, + + _DriveAdded: function(drive) { + var Dident = drive.enumerate_identifiers(); + var u_dev = drive.get_identifier('unix-device'); + var d_name = drive.get_name(); + log('DRIVE unix-device',u_dev,d_name) + this._drives[d_name] = new Object() + this._drives[d_name]['drive'] = drive; + this._drives[d_name]['device'] = drive.get_identifier('unix-device'); + //this._drives[d_name]['uuid'] = drive.get_identifier('uuid'); + this._drives[d_name]['volumes'] = new Object() + if (drive.has_volumes()) { + log(drive.get_volumes()); + var VList = drive.get_volumes(); + VList.forEach(Lang.bind(this, function(volume){ + var v_name = volume.get_name(); + log(v_name) + this._drives[d_name]['volumes'][v_name] = this._addVolume(volume); + })); + //VList.free() + }; + log('VOLUMES',JSON.stringify(this._drives)) + + this.drvsubmenu = new PopupMenu.PopupSubMenuMenuItem(drive.get_name(), true); + this.drvsubmenu.icon.icon_name = 'drive-harddisk-usb-symbolic'; + this.menu.addMenuItem(this.drvsubmenu); + + //log('Drive added',drive.get_name()) + //this.drvsubmenu = new PopupMenu.PopupSubMenuMenuItem(_(drive.get_name()), true); + //this.drvsubmenu.icon.icon_name = 'drive-harddisk-usb-symbolic'; + //this.drvsubmenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + }, + + _addVolume: function(volume) { + volume.enumerate_identifiers(); + var vol = new Object() + log(volume.get_identifier('uuid')) + log(volume.get_identifier('unix-device')) + log(volume.get_identifier('class')) + log(volume.get_identifier('label')) + vol['volume'] = volume; + vol['uuid'] = volume.get_identifier('uuid'); + vol['u_device'] = volume.get_identifier('unix-device'); + vol['vclass'] = volume.get_identifier('class'); + vol['label'] = volume.get_identifier('label'); + return vol + }, + + _DriveRemoved: function(drive) { + var me = this.menu._getMenuItems(); + this._removeItemByLabel(this.menu, drive.get_name()); + + + //this.drvsubmenu.destroy(); + }, + + _VolumeRemoved: function(volume) { + //Volume is removed, after it's mounted from system(d) + log('VOLUME REMOVED',volume.get_name()) + }, + + _VolumeAdded: function(volume) { + //Volume is added, after it's unmounted from system(d) + log('VOLUME CONNECTED',volume.get_name()) + log('ID',this._addedVolumeId) + if (volume.get_drive() == null) + return + var drive = volume.get_drive() + if ( !volume.can_mount() || !drive.is_removable()) + return + log(volume.get_mount()) + log(volume.enumerate_identifiers()) + log(volume.get_identifier('uuid')) + log(volume.get_identifier('unix-device')) + log(volume.get_identifier('class')) + log(volume.get_identifier('label')) + var dr = volume.get_drive() + log(dr.get_name()) + log(dr.enumerate_identifiers()) + log(dr.get_identifier('unix-device')) + + var me + try { + me = this.drvsubmenu.menu._getMenuItems(); + } catch(e) { + this._DriveAdded(drive) + } + me = this.drvsubmenu.menu._getMenuItems(); + + var menuItem = new VolMenuItem(volume, false); + + var VF = Gio.File.new_for_path('/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules'); + if (VF.query_exists(null)) { + log('UDEV exists','/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules') + menuItem.setToggleState(true); + } else { + log('UDEV not exists','/etc/udev/rules.d/99-ext-bkp-volume-u-'+volume.get_identifier('uuid')+'.rules') + menuItem.setToggleState(false); + } + + + //this._removeItemByLabel(this.drvsubmenu.menu, volume.get_name()); + this.drvsubmenu.menu.addMenuItem(menuItem); + var connID = menuItem.connect('toggled', Lang.bind(this, function() { + log('ACTIVE?',menuItem.state,menuItem.label.text) + if (menuItem.state) { + reg = 'register' + } else { + reg = 'unregister' + } + GLib.spawn_command_line_async( + this._getCommand('mkbackup-'+reg+'@'+volume.get_identifier('unix-device')+'.service', 'start', 'system')); + })); + log(connID,volume.get_name()) + this.menu.addMenuItem(this.drvsubmenu,1); + }, + + _removeItemByLabel: function(menu, label) { + log('LAB',label) + var children = menu._getMenuItems(); + for (var i = 0; i < children.length; i++) { + var item = children[i]; + log('REM',item.label.text,label) + if (item.label.text == label) + log('DESTROY',item.label.text) + //item.destroy(); + } + }, + + + + _addMount: function(mount) { + var item = new MountMenuItem(mount); + this._mounts.unshift(item); + this.menu.addMenuItem(item, 0); + }, + + _removeMount: function(mount) { + for (var i = 0; i < this._mounts.length; i++) { + var item = this._mounts[i]; + if (item.mount == mount) { + item.destroy(); + this._mounts.splice(i, 1); + return; + } + } + log ('Removing a mount that was never added to the menu'); + }, + + _run_command: function(COMMAND) { + var output = ""; + try { + output = GLib.spawn_command_line_sync(COMMAND, null, null, null, null); + } catch(e) { + throw e; + } + + return output[1].toString().replace(/\n$/, "") + ""; + }, + + + + _showVolume: function(volume) { + var drive = volume.get_drive(); + var mount = volume.get_mount(); + if (drive != null && drive.is_removable()){ + log('SVDRIVE',drive.get_name(),drive.is_removable()) + log('SVVOL',volume.get_name(),volume.get_uuid(),drive.is_removable()); + } + }, + + _showDrive: function(drive) { + //extMediaName = 'external backup-drive'; + log('SDDRIVE',drive.get_name(),drive.get_volumes(),drive.has_media(),drive.is_removable(),drive.has_volumes(),drive.enumerate_identifiers()); + if (drive.is_removable() && drive.has_volumes()) { + //extMediaName = drive.get_name(); + log('SDDREMOV') + + drive.get_volumes().forEach(Lang.bind(this, function(volume) { + if (volume.can_mount()){ + log('SDVOL',volume.get_name()); + if ( volume.get_mount() != null ) { + var mount = volume.get_mount(); + log('SDMR',mount.get_root()); + } + } + })); + } else { + log(drive.is_removable(),drive.has_volumes(),drive.get_volumes()); + } + }, + + _checkMount: function(mount) { + log('CHECK MOUNT '+mount.can_unmount()+' '+mount.can_eject()); + log('VOLUME '+mount.get_volume()); + return(mount.can_unmount() || mount.can_eject()) + }, + + _getCommand: function(service, action, type) { + var command = "systemctl" + + command += " " + action + command += " " + service + command += " --" + type + if (type == "system" && (action != 'is-active' && action != 'is-enabled')) + command = "pkexec --user root " + command + + return 'sh -c "' + command + '; exit;"' + }, + + _refresh_panel : function() { + + var active = false; + var volumes = [] + var mounted = false; + volumes.push(this.watchvolume) + + // TODO: find all Volumes on the drive, holding the backup and add this + // volumes to the list + volumes.push('home-jakob-Videos-extern.mount') + volumes.push('home-media.mount') + + this.aout = GLib.spawn_command_line_sync( + this._getCommand(this.services.join(' '), 'is-active', 'system'))[1].toString().split('\n'); + //log(this.aout); + + active = this.aout.indexOf('active') >= 0 + + var vout = GLib.spawn_command_line_sync( + this._getCommand(volumes.join(' '), 'is-active', 'system'))[1].toString().split('\n'); + mounted = vout.indexOf('active') >= 0 + + this.bkpsubmenu.icon.style = (active ? "color: #ff0000;" : "color: revert;"); + MainIcon.style = (mounted ? "color: #ff0000;" : "color: revert;"); + MainLabel.set_text(mounted ? _("mounted") : ""); + + if (this.menu.isOpen) { + //Menu is open + var me = this.menu._getMenuItems(); + me.forEach(Lang.bind(this, function(item) { + //log(item.label.text) + if ( item.label.text == extMediaName ) { + item.setToggleState(this._check_service('mkbackup@BKP.target','active')); + } + })); + if (this.bkpsubmenu.menu.isOpen) { + //Submenu Backu-intervals open + this._refresh(); + } + } + + if (this._refreshID != 0) + Mainloop.source_remove(this._refreshID); + this._refreshID = Mainloop.timeout_add_seconds(refreshTime, Lang.bind(this, this._refresh_panel)); + GLib.Source.set_name_by_id(this._refreshID, '[gnome-shell] this._refresh_panel'); + return false; + }, + + _check_service: function(service,stat) { + var [_, aout, aerr, astat] = GLib.spawn_command_line_sync( + this._getCommand(service, 'is-'+stat, 'system')); + return (astat == 0); + }, + + _refresh: function() { + var me = this.bkpsubmenu.menu._getMenuItems(); + //log(this.services.join(' ')) + + var eout = GLib.spawn_command_line_sync( + this._getCommand(this.services.join(' '), 'is-enabled', 'system'))[1].toString().split('\n'); + //log(eout); + + + this._entries.forEach(Lang.bind(this, function(service,index,arr) { + if (! arr[index].enabled == (eout[index] == 'enabled')) + arr[index].changed = true + arr[index].enabled = (eout[index] == 'enabled' ? true : false) + + if (! arr[index].active == (this.aout[index] == 'active')) + arr[index].changed = true + arr[index].active = (this.aout[index] == 'active' ? true : false) + })); + + this._entries.forEach(Lang.bind(this, function(service,index,arr) { + var serviceItem + me.forEach(Lang.bind(this, function(item) { + if ( item.label.text == service['name'] ) { + arr[index].found = true; + serviceItem = item; + me.splice(me.indexOf(item),1); + } + })); + + if ( arr[index].changed ) { + if (arr[index].found) { + //log('update',service['name']); + if ( service.staticserv ) { + serviceItem.setToggleState(service.active); + } else { + serviceItem.setToggleState(service.enabled); + } + } else { + //log('new',service['name']); + if ( service.staticserv ) { + serviceItem = new PopupTargetItem(service['name'], service.active); + this.bkpsubmenu.menu.addMenuItem(serviceItem); + + serviceItem.connect('toggled', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand(service['service'], (this._check_service(service.service, 'active') ? 'stop' : 'start'), service["type"])); + })); + } else { + serviceItem = new PopupServiceItem(service['name'], service.enabled); + this.bkpsubmenu.menu.addMenuItem(serviceItem); + + serviceItem.connect('toggled', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand(service['service'], (this._check_service(service.service, 'enabled') ? 'disable' : 'enable'), service["type"])); + })); + + serviceItem.actionButton.connect('clicked', Lang.bind(this, function() { + GLib.spawn_command_line_async( + this._getCommand(service['service'], (this._check_service(service.service, 'active') ? 'stop' : 'start'), service["type"])); + this.menu.close(); + })); + } + } + if (serviceItem.actionButton.child) { + serviceItem.actionButton.child.icon_name = (service.active ? EnabledIcon : DisabledIcon); + serviceItem.actionButton.child.style = (service.active ? "color: #ff0000;" : "color: revert;"); + }; + if (serviceItem.transferButton) { + serviceItem.transferButton.style = (service.tr ? "text-decoration: revert;" : "text-decoration: line-through;"); + }; + + + //log('Changed',service.service) + } + //log('X',arr[index].service,arr[index].enabled,arr[index].active,arr[index].changed) + arr[index].changed = false; + })); + + this.bkpsubmenu.menu._getMenuItems().forEach(Lang.bind(this, function(item) { + var mic = 0 + if ( me.length > mic ) { + for (var i = mic; i < me.length; i++) { + if (item == me[i]) { + log('DESTROY',me[i].label.text); + item.destroy(); + } + } + } + })); + + return true; + }, + + _loadConfig: function() { + //log('LOAD CONFIG') + var intervals + this.services = [] + var kf = new GLib.KeyFile() + var obj = new Object(); + this._entries = []; + + if(kf.load_from_file('/etc/mksnapshot.conf',GLib.KeyFileFlags.NONE)){ + //intervals = kf.get_groups()[0]; + this.bkpmnt = kf.get_value('DEFAULT','bkpmnt') + + //log('BKP',this.bkppath) + kf.get_groups()[0].forEach(Lang.bind(this, function(interval) { + var obj = new Object(); + var i = "" + if (interval === 'DEFAULT') + i = 'misc' + else + i = interval + obj.name = i +' backups'; + obj.interval = interval; + obj.service = "mkbackup@"+i+".service"; + this.services.push(obj.service) + obj.type = "system"; + obj.enabled = false; + obj.active = false; + obj.changed = true; + obj.found = false; + obj.staticserv = false; + + try { + obj.tr = (kf.get_value(interval,"transfer").toLowerCase() === "true"); + } catch(err) { + obj.tr = (kf.get_value("DEFAULT","transfer").toLowerCase() === "true"); + } + this._entries.push(obj)})); + } + } +}); + +var backupManager; + +function init(extensionMeta) { + //Convenience.initTranslations(); + var theme = imports.gi.Gtk.IconTheme.get_default(); + theme.append_search_path(extensionMeta.path + "/icons"); +} + + +function enable() { + backupManager = new BackupManager(); + +} + +function disable() { + backupManager.destroy(); +} diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/icons/my-caffeine-off-symbolic.svg b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/icons/my-caffeine-off-symbolic.svg new file mode 100644 index 0000000..d531087 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/icons/my-caffeine-off-symbolic.svg @@ -0,0 +1,178 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + + + + + + + diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/icons/my-caffeine-on-symbolic.svg b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/icons/my-caffeine-on-symbolic.svg new file mode 100644 index 0000000..5836586 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/icons/my-caffeine-on-symbolic.svg @@ -0,0 +1,178 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + + + + + + + + diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/metadata.json b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/metadata.json new file mode 100755 index 0000000..60d1478 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/metadata.json @@ -0,0 +1,16 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Toggle systemd services on/off from a popup menu in the top gnome panel. Can be used to start services like apache2, mysql, postgres. It uses `pkexec' to run `sytemctl'. If you want to start services without entering a password you have to polkit policy file. An example policy file can be found in the github repository.", + "name": "Zeitmaschine", + "settings-schema": "org.gnome.shell.extensions.zeitmaschine", + "shell-version": [ + "3.14", + "3.16", + "3.18", + "3.20", + "3.22" + ], + "url": "https://github.com/xundeenergie/gnome-shell-extension-zeitmaschine", + "uuid": "zeitmaschine@xundeenergie.at", + "version": 11 +} diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupBkpVolumItem.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupBkpVolumItem.js new file mode 100755 index 0000000..279d9fe --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupBkpVolumItem.js @@ -0,0 +1,113 @@ +var Lang = imports.lang; +var PopupMenu = imports.ui.popupMenu; +var St = imports.gi.St; +var Clutter = imports.gi.Clutter; +var Gio = imports.gi.Gio; +var Util = imports.misc.util; +var Gtk = imports.gi.Gtk; + +var ExtensionSystem = imports.ui.extensionSystem; +var ExtensionUtils = imports.misc.extensionUtils; +var DisabledIcon = 'my-caffeine-off-symbolic'; +//var DisabledIcon = 'gnome-spinner'; + +var PopupBKPItem = new Lang.Class({ + Name: 'PopupBKPItem', + Extends: PopupMenu.PopupSwitchMenuItem, + + _init: function(mount, active, params) { + this.parent(mount.get_name(), active, params); + log('YY',mount.get_name()) + + this.label = new St.Label({ text: mount.get_name()}); + this.actor.add(this.label, { expand: true }); + this.actor.label_actor = this.label; + + this.mount = mount; + + var ejectIcon = new St.Icon({ icon_name: 'media-eject-symbolic', + style_class: 'popup-menu-icon ' }); + var ejectButton = new St.Button({ child: ejectIcon }); + ejectButton.connect('clicked', Lang.bind(this, this._eject)); + this.actor.add(ejectButton); + + this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility)); + this._syncVisibility(); + }, + + destroy: function() { + if (this._changedId) { + this.mount.disconnect(this._changedId); + this._changedId = 0; + } + + this.parent(); + }, + + _isInteresting: function() { + //return true + if (!this.mount.can_eject() && !this.mount.can_unmount()) + return false; + if (this.mount.is_shadowed()) + return false; + + var volume = this.mount.get_volume(); + + if (volume == null) { + // probably a GDaemonMount, could be network or + // local, but we can't tell; assume it's local for now + return true; + } + + return volume.get_identifier('class') != 'network'; + }, + + _syncVisibility: function() { + this.actor.visible = this._isInteresting(); + }, + + _eject: function() { + var mountOp = new ShellMountOperation.ShellMountOperation(this.mount); + + if (this.mount.can_eject()) + this.mount.eject_with_operation(Gio.MountUnmountFlags.NONE, + mountOp.mountOp, + null, // Gio.Cancellable + Lang.bind(this, this._ejectFinish)); + else + this.mount.unmount_with_operation(Gio.MountUnmountFlags.NONE, + mountOp.mountOp, + null, // Gio.Cancellable + Lang.bind(this, this._unmountFinish)); + }, + + _unmountFinish: function(mount, result) { + try { + mount.unmount_with_operation_finish(result); + } catch(e) { + this._reportFailure(e); + } + }, + + _ejectFinish: function(mount, result) { + try { + mount.eject_with_operation_finish(result); + } catch(e) { + this._reportFailure(e); + } + }, + + _reportFailure: function(exception) { + var msg = _("Ejecting drive '%s' failed:").format(this.mount.get_name()); + Main.notifyError(msg, exception.message); + }, + + /*activate: function(event) { + var context = global.create_app_launch_context(event.get_time(), -1); + Gio.AppInfo.launch_default_for_uri(this.mount.get_root().get_uri(), + context); + + this.parent(event); + }*/ +}); + diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupDriveItem.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupDriveItem.js new file mode 100644 index 0000000..3bce00f --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupDriveItem.js @@ -0,0 +1,35 @@ +var Lang = imports.lang; +var PopupMenu = imports.ui.popupMenu; +var St = imports.gi.St; +var Clutter = imports.gi.Clutter; +var Util = imports.misc.util; +var Gtk = imports.gi.Gtk; + +var ExtensionSystem = imports.ui.extensionSystem; +var ExtensionUtils = imports.misc.extensionUtils; + +var DriveMenuItem = new Lang.Class({ + Name: 'DriveMenuItem', + Extends: PopupMenu.PopupBaseMenuItem, + + _init: function(drive) { + this.parent(); + + this.label = new St.Label({ text: drive.get_name() }); + this.actor.add(this.label, { expand: true }); + this.actor.label_actor = this.label; + + this.drive = drive; + + var ejectIcon = new St.Icon({ icon_name: 'drive-harddisk-usb-symbolic', + style_class: 'popup-menu-icon ' }); + //var ejectIcon = mount.get_icon(); + var ejectButton = new St.Button({ child: ejectIcon }); +// ejectButton.connect('clicked', Lang.bind(this, this._eject)); + this.actor.add(ejectButton); + +// this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility)); +// this._syncVisibility(); + } + +}); diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupManuallyItem.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupManuallyItem.js new file mode 100644 index 0000000..155a8c9 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupManuallyItem.js @@ -0,0 +1,42 @@ +var Lang = imports.lang; +var PopupMenu = imports.ui.popupMenu; +var St = imports.gi.St; +var Clutter = imports.gi.Clutter; +var Util = imports.misc.util; +var Gtk = imports.gi.Gtk; + +var ExtensionSystem = imports.ui.extensionSystem; +var ExtensionUtils = imports.misc.extensionUtils; +var DisabledIcon = 'my-caffeine-off-symbolic'; +//var DisabledIcon = 'gnome-spinner'; + +var PopupServiceItem = new Lang.Class({ + Name: 'PopupServiceItem', + Extends: PopupMenu.PopupMenuItem, + + _init: function(text, active, params) { + this.parent(text, active, params); + + this.actionButton = new St.Button({ + x_align: 1, + reactive: true, + can_focus: true, + track_hover: true, + accessible_name: 'restart', + style_class: 'system-menu-action services-systemd-button-reload' }); + + var icon = new St.Icon({ icon_name: DisabledIcon }) + this.actionButton.child = icon; + this.actor.add(this.actionButton, { expand: false, x_align: St.Align.END }); + + /*this.ejectButton = new St.Button({ x_align: 1, + reactive: true, + can_focus: true, + track_hover: true, + accessible_name: 'eject', + style_class: 'system-menu-action services-systemd-button-reload' }); + + this.ejectButton.child = new St.Icon({ icon_name: 'media-eject-symbolic' }); + this.actor.add(this.ejectButton, { expand: false, x_align: St.Align.END });*/ + } +}); diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupMountItem.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupMountItem.js new file mode 100644 index 0000000..6dff6f0 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupMountItem.js @@ -0,0 +1,108 @@ +var Lang = imports.lang; +var PopupMenu = imports.ui.popupMenu; +var St = imports.gi.St; +var Clutter = imports.gi.Clutter; +var Util = imports.misc.util; +var Gtk = imports.gi.Gtk; + +var ExtensionSystem = imports.ui.extensionSystem; +var ExtensionUtils = imports.misc.extensionUtils; + +var MountMenuItem = new Lang.Class({ + Name: 'MountMenuItem', + Extends: PopupMenu.PopupBaseMenuItem, + + _init: function(mount) { + this.parent(); + + this.label = new St.Label({ text: mount.get_name() }); + this.actor.add(this.label, { expand: true }); + this.actor.label_actor = this.label; + + this.mount = mount; + + var ejectIcon = new St.Icon({ icon_name: 'media-eject-symbolic', + style_class: 'popup-menu-icon ' }); + //var ejectIcon = mount.get_icon(); + var ejectButton = new St.Button({ child: ejectIcon }); + ejectButton.connect('clicked', Lang.bind(this, this._eject)); + this.actor.add(ejectButton); + + this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility)); + this._syncVisibility(); + }, + + destroy: function() { + if (this._changedId) { + this.mount.disconnect(this._changedId); + this._changedId = 0; + } + + this.parent(); + }, + + _isInteresting: function() { + if (!this.mount.can_eject() && !this.mount.can_unmount()) + return false; + if (this.mount.is_shadowed()) + return false; + + var volume = this.mount.get_volume(); + + if (volume == null) { + // probably a GDaemonMount, could be network or + // local, but we can't tell; assume it's local for now + return true; + } + + return volume.get_identifier('class') != 'network'; + }, + + _syncVisibility: function() { + this.actor.visible = this._isInteresting(); + }, + + _eject: function() { + var mountOp = new ShellMountOperation.ShellMountOperation(this.mount); + + if (this.mount.can_eject()) + this.mount.eject_with_operation(Gio.MountUnmountFlags.NONE, + mountOp.mountOp, + null, // Gio.Cancellable + Lang.bind(this, this._ejectFinish)); + else + this.mount.unmount_with_operation(Gio.MountUnmountFlags.NONE, + mountOp.mountOp, + null, // Gio.Cancellable + Lang.bind(this, this._unmountFinish)); + }, + + _unmountFinish: function(mount, result) { + try { + mount.unmount_with_operation_finish(result); + } catch(e) { + this._reportFailure(e); + } + }, + + _ejectFinish: function(mount, result) { + try { + mount.eject_with_operation_finish(result); + } catch(e) { + this._reportFailure(e); + } + }, + + _reportFailure: function(exception) { + var msg = _("Ejecting drive '%s' failed:").format(this.mount.get_name()); + Main.notifyError(msg, exception.message); + }, + + activate: function(event) { + var context = global.create_app_launch_context(event.get_time(), -1); + Gio.AppInfo.launch_default_for_uri(this.mount.get_root().get_uri(), + context); + + this.parent(event); + } +}); diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupServiceItem.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupServiceItem.js new file mode 100644 index 0000000..39e6b94 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupServiceItem.js @@ -0,0 +1,55 @@ +var Lang = imports.lang; +var PopupMenu = imports.ui.popupMenu; +var St = imports.gi.St; +var Clutter = imports.gi.Clutter; +var Util = imports.misc.util; +var Gtk = imports.gi.Gtk; + +var ExtensionSystem = imports.ui.extensionSystem; +var ExtensionUtils = imports.misc.extensionUtils; +var DisabledIcon = 'my-caffeine-off-symbolic'; +var description = "Beschreibung"; +//var DisabledIcon = 'gnome-spinner'; + +var PopupServiceItem = new Lang.Class({ + Name: 'PopupServiceItem', + Extends: PopupMenu.PopupSwitchMenuItem, + + _init: function(text, active, params) { + this.parent(text, active, params); + +/* this.descriptionLabel = new St.Button({ + label: description, + reactive: false, + x_align: St.Align.START, + can_focus: false, + accessible_name: 'description'}); + this.actor.add(this.descriptionLabel, {expand: true}); +*/ + this.actionButton = new St.Button({ + x_align: 1, + reactive: true, + can_focus: true, + track_hover: true, + accessible_name: 'restart', + style_class: 'system-menu-action services-systemd-button-reload' }); + + var icon = new St.Icon({ icon_name: DisabledIcon }) + this.actionButton.child = icon; + this.actor.add(this.actionButton, { expand: false, x_align: St.Align.END }); + + this.transferButton = new St.Button({ + label: 'transfer', + x_align: 1, + reactive: true, + can_focus: true, + track_hover: true, + accessible_name: 'transfer', + style_class: 'system-menu-action services-systemd-button-transfer' }); + + //this.transferButton.child = new St.Icon({ icon_name: 'media-eject-symbolic' }); + this.actor.add(this.transferButton, { expand: false, x_align: St.Align.END }); + + }, + +}); diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupTargetItem.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupTargetItem.js new file mode 100755 index 0000000..183e472 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupTargetItem.js @@ -0,0 +1,27 @@ +var Lang = imports.lang; +var PopupMenu = imports.ui.popupMenu; +var St = imports.gi.St; +var Clutter = imports.gi.Clutter; +var Util = imports.misc.util; +var Gtk = imports.gi.Gtk; + +var ExtensionSystem = imports.ui.extensionSystem; +var ExtensionUtils = imports.misc.extensionUtils; + +var PopupTargetItem = new Lang.Class({ + Name: 'PopupServiceItem', + Extends: PopupMenu.PopupSwitchMenuItem, + + _init: function(text, active, params) { + this.parent(text, active, params); + + this.actionButton = new St.Button({ x_align: 1, + reactive: true, + can_focus: true, + track_hover: true, + accessible_name: 'restart', + style_class: 'system-menu-action services-systemd-button-reload' }); + + }}); + + diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupTestMenuItem.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupTestMenuItem.js new file mode 100644 index 0000000..6dff6f0 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/popupTestMenuItem.js @@ -0,0 +1,108 @@ +var Lang = imports.lang; +var PopupMenu = imports.ui.popupMenu; +var St = imports.gi.St; +var Clutter = imports.gi.Clutter; +var Util = imports.misc.util; +var Gtk = imports.gi.Gtk; + +var ExtensionSystem = imports.ui.extensionSystem; +var ExtensionUtils = imports.misc.extensionUtils; + +var MountMenuItem = new Lang.Class({ + Name: 'MountMenuItem', + Extends: PopupMenu.PopupBaseMenuItem, + + _init: function(mount) { + this.parent(); + + this.label = new St.Label({ text: mount.get_name() }); + this.actor.add(this.label, { expand: true }); + this.actor.label_actor = this.label; + + this.mount = mount; + + var ejectIcon = new St.Icon({ icon_name: 'media-eject-symbolic', + style_class: 'popup-menu-icon ' }); + //var ejectIcon = mount.get_icon(); + var ejectButton = new St.Button({ child: ejectIcon }); + ejectButton.connect('clicked', Lang.bind(this, this._eject)); + this.actor.add(ejectButton); + + this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility)); + this._syncVisibility(); + }, + + destroy: function() { + if (this._changedId) { + this.mount.disconnect(this._changedId); + this._changedId = 0; + } + + this.parent(); + }, + + _isInteresting: function() { + if (!this.mount.can_eject() && !this.mount.can_unmount()) + return false; + if (this.mount.is_shadowed()) + return false; + + var volume = this.mount.get_volume(); + + if (volume == null) { + // probably a GDaemonMount, could be network or + // local, but we can't tell; assume it's local for now + return true; + } + + return volume.get_identifier('class') != 'network'; + }, + + _syncVisibility: function() { + this.actor.visible = this._isInteresting(); + }, + + _eject: function() { + var mountOp = new ShellMountOperation.ShellMountOperation(this.mount); + + if (this.mount.can_eject()) + this.mount.eject_with_operation(Gio.MountUnmountFlags.NONE, + mountOp.mountOp, + null, // Gio.Cancellable + Lang.bind(this, this._ejectFinish)); + else + this.mount.unmount_with_operation(Gio.MountUnmountFlags.NONE, + mountOp.mountOp, + null, // Gio.Cancellable + Lang.bind(this, this._unmountFinish)); + }, + + _unmountFinish: function(mount, result) { + try { + mount.unmount_with_operation_finish(result); + } catch(e) { + this._reportFailure(e); + } + }, + + _ejectFinish: function(mount, result) { + try { + mount.eject_with_operation_finish(result); + } catch(e) { + this._reportFailure(e); + } + }, + + _reportFailure: function(exception) { + var msg = _("Ejecting drive '%s' failed:").format(this.mount.get_name()); + Main.notifyError(msg, exception.message); + }, + + activate: function(event) { + var context = global.create_app_launch_context(event.get_time(), -1); + Gio.AppInfo.launch_default_for_uri(this.mount.get_root().get_uri(), + context); + + this.parent(event); + } +}); diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/prefs.js b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/prefs.js new file mode 100644 index 0000000..b4049da --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/prefs.js @@ -0,0 +1,338 @@ +var GLib = imports.gi.GLib; +var Gio = imports.gi.Gio; +var Gtk = imports.gi.Gtk; +var GObject = imports.gi.GObject; +var Lang = imports.lang; + +var ExtensionUtils = imports.misc.extensionUtils; +var Me = ExtensionUtils.getCurrentExtension(); +var Convenience = Me.imports.convenience; + + +var ServicesSystemdSettings = new GObject.Class({ + Name: 'Services-Systemd-Settings', + Extends: Gtk.Grid, + + _init : function(params) { + // Gtk Grid init + this.parent(params); + this.set_orientation(Gtk.Orientation.VERTICAL); + this.margin = 20; + + // Open settings + this._settings = Convenience.getSettings(); + this._settings.connect('changed', Lang.bind(this, this._refresh)); + + this._changedPermitted = false; + + // Label + var treeViewLabel = new Gtk.Label({ label: '' + "Listed systemd Services:" + '', + use_markup: true, + halign: Gtk.Align.START }) + this.add(treeViewLabel); + + + // TreeView + this._store = new Gtk.ListStore(); + this._store.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING]); + + this._treeView = new Gtk.TreeView({ model: this._store, + hexpand: true, vexpand: true }); + + var selection = this._treeView.get_selection(); + selection.set_mode(Gtk.SelectionMode.SINGLE); + selection.connect ('changed', Lang.bind (this, this._onSelectionChanged)); + + + var appColumn = new Gtk.TreeViewColumn({ expand: true, + title: "Label" }); + + var nameRenderer = new Gtk.CellRendererText; + appColumn.pack_start(nameRenderer, true); + appColumn.add_attribute(nameRenderer, "text", 0); + this._treeView.append_column(appColumn); + + var appColumn = new Gtk.TreeViewColumn({ expand: true, + title: "Service" }); + + var nameRenderer = new Gtk.CellRendererText; + appColumn.pack_start(nameRenderer, true); + appColumn.add_attribute(nameRenderer, "text", 1); + this._treeView.append_column(appColumn); + + var appColumn = new Gtk.TreeViewColumn({ expand: true, + title: "Type" }); + + var nameRenderer = new Gtk.CellRendererText; + appColumn.pack_start(nameRenderer, true); + appColumn.add_attribute(nameRenderer, "text", 2); + this._treeView.append_column(appColumn); + + this.add(this._treeView); + + // Devare Toolbar + var toolbar = new Gtk.Toolbar(); + toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR); + toolbar.halign = 2; + this.add(toolbar); + + var upButton = new Gtk.ToolButton({ stock_id: Gtk.STOCK_GO_UP }); + upButton.connect('clicked', Lang.bind(this, this._up)); + toolbar.add(upButton); + + var downButton = new Gtk.ToolButton({ stock_id: Gtk.STOCK_GO_DOWN }); + downButton.connect('clicked', Lang.bind(this, this._down)); + toolbar.add(downButton); + + var delButton = new Gtk.ToolButton({ stock_id: Gtk.STOCK_DELETE }); + delButton.connect('clicked', Lang.bind(this, this._devare)); + toolbar.add(delButton); + + this._selDepButtons = [upButton, downButton, delButton] + + // Add Grid + var grid = new Gtk.Grid(); + + //// Label + var labelName = new Gtk.Label({label: "Label: "}); + labelName.halign = 2; + + this._displayName = new Gtk.Entry({ hexpand: true, + margin_top: 5 }); + this._displayName.set_placeholder_text("Name in menu"); + + var labelService = new Gtk.Label({label: "Service: "}); + labelService.halign = 2; + + this._availableSystemdServices = { + //'system': this._getSystemdServicesList("system"), + //'user': this._getSystemdServicesList("user"), + 'system': this._getSystemdTargetsList("system"), + 'user': this._getSystemdTargetsList("user"), + } + this._availableSystemdServices['all'] = this._availableSystemdServices['system'].concat(this._availableSystemdServices['user']) + //this._availableSystemdServices['all'] = this._availableSystemdServices['systemtargets'].concat(this._availableSystemdServices['all']) + //this._availableSystemdServices['all'] = this._availableSystemdServices['usertargets'].concat(this._availableSystemdServices['all']) + + var sListStore = new Gtk.ListStore(); + sListStore.set_column_types([GObject.TYPE_STRING, GObject.TYPE_INT]); + + for (var i in this._availableSystemdServices['all']) + sListStore.set (sListStore.append(), [0], [this._availableSystemdServices['all'][i]]); + + this._systemName = new Gtk.Entry() + this._systemName.set_placeholder_text("Systemd service name"); + var compvarion = new Gtk.EntryCompvarion() + this._systemName.set_compvarion(compvarion) + compvarion.set_model(sListStore) + + compvarion.set_text_column(0) + + grid.attach(labelName, 1, 1, 1, 1); + grid.attach_next_to(this._displayName, labelName, 1, 1, 1); + + grid.attach(labelService, 1, 2, 1, 1); + grid.attach_next_to(this._systemName,labelService, 1, 1, 1); + + this.add(grid); + + var toolbar = new Gtk.Toolbar(); + toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR); + toolbar.halign = 2; + this.add(toolbar); + + var addButton = new Gtk.ToolButton({ stock_id: Gtk.STOCK_ADD, + label: "Add", + is_important: true }); + + addButton.connect('clicked', Lang.bind(this, this._add)); + toolbar.add(addButton); + + this._changedPermitted = true; + this._refresh(); + this._onSelectionChanged(); + }, + + _getSystemdTargetsList: function(type) { + var [_, out, err, stat] = GLib.spawn_command_line_sync('sh -c "systemctl --' + type + ' list-unit-files --type=target | tail -n +2 | head -n -2 | awk \'{print $1}\'"'); + var allFiltered = out.toString().split("\n"); + return allFiltered.sort( + function (a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }) + }, + + _getSystemdServicesList: function(type) { + var [_, out, err, stat] = GLib.spawn_command_line_sync('sh -c "systemctl --' + type + ' list-unit-files --type=service | tail -n +2 | head -n -2 | awk \'{print $1}\'"'); + var allFiltered = out.toString().split("\n"); + return allFiltered.sort( + function (a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }) + }, + _getTypeOfService: function(service) { + var type = "undefined" + if (this._availableSystemdServices['systemtargets'].indexOf(service) != -1 && this._availableSystemdServices['system'].indexOf(service) != -1) + type = "system" + else if (this._availableSystemdServices['usertargets'].indexOf(service) != -1 && this._availableSystemdServices['user'].indexOf(service) != -1) + type = "user" + return type + }, + + _getIdFromIter: function(iter) { + var displayName = this._store.get_value(iter, 0); + var serviceName = this._store.get_value(iter, 1); + var type = this._store.get_value(iter, 2); + return JSON.stringify({"name": displayName, "service": serviceName, "type": type}); + }, + + _add: function() { + var displayName = this._displayName.text + var serviceName = this._systemName.text + + if (displayName.trim().length > 0 && serviceName.trim().length > 0 ) { + var type = this._getTypeOfService(serviceName) + if (type == "undefined") { + this._messageDialog = new Gtk.MessageDialog ({ + title: "Warning", + modal: true, + buttons: Gtk.ButtonsType.OK, + message_type: Gtk.MessageType.WARNING, + text: "Service does not exist." + }); + this._messageDialog.connect('response', Lang.bind(this, function() { + this._messageDialog.close(); + })); + this._messageDialog.show(); + } else { + var id = JSON.stringify({"name": displayName, "service": serviceName, "type": type}) + var currentItems = this._settings.get_strv("zeitmaschine"); + var index = currentItems.indexOf(id); + if (index < 0) { + this._changedPermitted = false; + currentItems.push(id); + this._settings.set_strv("zeitmaschine", currentItems); + this._store.set(this._store.append(), [0, 1, 2], [displayName, serviceName, type]); + this._changedPermitted = true; + } + this._displayName.text = "" + this._systemName.text = "" + } + + } else { + this._messageDialog = new Gtk.MessageDialog ({ + //parent: this.get_toplevel(), + title: "Warning", + modal: true, + buttons: Gtk.ButtonsType.OK, + message_type: Gtk.MessageType.WARNING, + text: "No label and/or service specified." + }); + + this._messageDialog.connect ('response', Lang.bind(this, function() { + this._messageDialog.close(); + })); + this._messageDialog.show(); + } + }, + + _up: function() { + var [any, model, iter] = this._treeView.get_selection().get_selected(); + + if (any) { + var index = this._settings.get_strv("zeitmaschine").indexOf(this._getIdFromIter(iter)); + this._move(index, index - 1) + } + }, + _down: function() { + var [any, model, iter] = this._treeView.get_selection().get_selected(); + + if (any) { + var index = this._settings.get_strv("zeitmaschine").indexOf(this._getIdFromIter(iter)); + this._move(index, index + 1) + } + }, + _move: function(oldIndex, newIndex) { + var currentItems = this._settings.get_strv("zeitmaschine"); + + if (oldIndex < 0 || oldIndex >= currentItems.length || + newIndex < 0 || newIndex >= currentItems.length) + return; + + currentItems.splice(newIndex, 0, currentItems.splice(oldIndex, 1)[0]); + + this._settings.set_strv("zeitmaschine", currentItems); + + this._treeView.get_selection().unselect_all(); + this._treeView.get_selection().select_path(Gtk.TreePath.new_from_string(String(newIndex))); + }, + _devare: function() { + var [any, model, iter] = this._treeView.get_selection().get_selected(); + + if (any) { + var currentItems = this._settings.get_strv("zeitmaschine"); + var index = currentItems.indexOf(this._getIdFromIter(iter)); + + if (index < 0) + return; + + currentItems.splice(index, 1); + this._settings.set_strv("zeitmaschine", currentItems); + + this._store.remove(iter); + } + }, + _onSelectionChanged: function() { + var [any, model, iter] = this._treeView.get_selection().get_selected(); + if (any) { + this._selDepButtons.forEach(function(value) { + value.set_sensitive(true) + }); + } else { + this._selDepButtons.forEach(function(value) { + value.set_sensitive(false) + }); + } + }, + _refresh: function() { + if (!this._changedPermitted) + return; + + this._store.clear(); + + var currentItems = this._settings.get_strv("zeitmaschien"); + var validItems = [ ]; + + for (var i = 0; i < currentItems.length; i++) { + var entry = JSON.parse(currentItems[i]); + // REMOVE NOT EXISTING ENTRIES + if (this._availableSystemdServices["all"].indexOf(entry["service"]) < 0) + continue; + + // COMPABILITY + if(!("type" in entry)) + entry["type"] = this._getTypeOfService(entry["service"]) + + validItems.push(JSON.stringify(entry)); + + var iter = this._store.append(); + this._store.set(iter, + [0, 1, 2], + [entry["name"], entry["service"], entry["type"]]); + } + + this._changedPermitted = false + this._settings.set_strv("zeitmaschine", validItems); + this._changedPermitted = true + } +}); + +function init() { +} + +function buildPrefsWidget() { + var widget = new ServicesSystemdSettings(); + widget.show_all(); + + return widget; +} diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/schemas/gschemas.compiled b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/schemas/gschemas.compiled new file mode 100644 index 0000000..784d5a0 Binary files /dev/null and b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/schemas/gschemas.compiled differ diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/schemas/org.gnome.shell.extensions.zeitmaschine.gschema.xml b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/schemas/org.gnome.shell.extensions.zeitmaschine.gschema.xml new file mode 100644 index 0000000..6fd0b39 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/schemas/org.gnome.shell.extensions.zeitmaschine.gschema.xml @@ -0,0 +1,10 @@ + + + + + [ ] + Systemd service list + A list of serrvice which are shown + + + diff --git a/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/stylesheet.css b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/stylesheet.css new file mode 100644 index 0000000..4cd7014 --- /dev/null +++ b/files/usr/share/gnome-shell/extensions/zeitmaschine@xundeenergie.at/stylesheet.css @@ -0,0 +1,23 @@ +.system-menu-action.services-systemd-button-transfer { + height: 18px; + padding: 0px; + border: 0px; +} + +.system-menu-action.services-systemd-button-reload { + height: 18px; + width: 18px; + padding: 0px; + border: 0px; +} + +.disk-active-icon { + icon-size: 1.09em; + padding: 0 5px; + color: #ff0000; +} +.system-status-icon-red { + icon-size: 1.09em; + padding: 0 5px; + color: #ff0000; +} diff --git a/files/var/lib/gdm3/.config/autostart/mkbackup-notification.desktop b/files/var/lib/gdm3/.config/autostart/mkbackup-notification.desktop new file mode 100644 index 0000000..9fe9ce2 --- /dev/null +++ b/files/var/lib/gdm3/.config/autostart/mkbackup-notification.desktop @@ -0,0 +1,107 @@ +[Desktop Entry] +Version=1.0 +Name=Notifications service for mkbackup +Name[ar]=تنبيهات +Name[ast]=Notificaciones +Name[bg]=Уведомления +Name[ca]=Notificacions +Name[cs]=Oznámení +Name[da]=Beskeder +Name[de]=Benachrichtigungen für mkbackup +Name[el]=Ειδοποιήσεις +Name[en_AU]=Notifications +Name[en_GB]=Notifications +Name[es]=Notificaciones +Name[eu]=Berri-emateak +Name[fi]=Ilmoitukset +Name[fr]=Notifications +Name[gl]=Notificacións +Name[he]=התראות +Name[hr]=Obavijesti +Name[hu]=Értesítések +Name[id]=Notifikasi +Name[is]=Tilkynningar +Name[it]=Notifiche +Name[ja]=通知 +Name[kk]=Хабарламалар +Name[ko]=알림 +Name[lt]=Pranešimai +Name[lv]=Paziņojumi +Name[ms]=Pemberitahuan +Name[nb]=Varsling +Name[nl]=Meldingen +Name[oc]=Notificacions +Name[pa]=ਨੋਟੀਫਿਕੇਸ਼ਨ +Name[pl]=Powiadomienia +Name[pt]=Notificações +Name[pt_BR]=Notificações +Name[ro]=Notificări +Name[ru]=Оповещения +Name[sk]=Oznámenia +Name[sl]=Obvestila +Name[sq]=Njoftime +Name[sr]=Обавештења +Name[sv]=Notifieringar +Name[th]=การแจ้งเหตุ +Name[tr]=Bildiriler +Name[ug]=ئۇقتۇرۇشلار +Name[uk]=Сповіщення +Name[vi]=Thông báo +Name[zh_CN]=通知 +Name[zh_TW]=通知 +Comment=Customize how notifications appear on your screen +Comment[ar]=خصص كيف تظهر التنبيهات على الشاشة +Comment[ast]=Personaliza cómo apaecen les notificaciones na to pantalla +Comment[bg]=Настройване на изгледа на уведомленията на екрана +Comment[ca]=Personalitzeu com es mostren les notificacions +Comment[cs]=Upravte způsob, jakým se budou oznámení zobrazovat +Comment[da]=Tilpas hvordan beskeder fremkommer på din skærm +Comment[de]=Das Erscheinungsbild von Benachrichtigungen anpassen +Comment[el]=Προσαρμογή του τρόπου εμφάνισης των ειδοποιήσεων στην οθόνη +Comment[en_AU]=Customise how notifications appear on your screen +Comment[en_GB]=Customise how notifications appear on your screen +Comment[es]=Personalice cómo aparecen las notificaciones en pantalla +Comment[eu]=Pertsonalizatu berri-emateak zure pantailan nola agertuko diren +Comment[fi]=Mukauta näytöllesi ilmestyvien ilmoitusten toimintaa +Comment[fr]=Personnaliser la manière dont les notifications apparaissent sur votre écran +Comment[gl]=Personalice como se mostran as notificacións na súa pantalla +Comment[he]=התאם מראה התראות על המסך שלך +Comment[hr]=Prilagodite kako će se obavijesti prikazivati na vašem ekranu +Comment[hu]=A képernyőn megjelenő értesítések megjelenésének személyre szabása +Comment[id]=Sesuaikan bagaimana notifikasi tampak di layar anda +Comment[is]=Sérsníða hvernig tilkynningar birtast á skjánum þínum +Comment[it]=Personalizzazione del modo in cui le notifiche appaiono sullo schermo +Comment[ja]=画面上にどのように通知するか設定します +Comment[kk]=Хабарламалар көрсетілуін таңдаңыз +Comment[ko]=알림을 화면에 어떻게 띄울 것인지 설정합니다 +Comment[lt]=Tinkinti kaip atrodys pranešimai jūsų ekrane +Comment[lv]=Pielāgojiet, kā paziņojumu parādās uz jūsu ekrāna +Comment[ms]=Suaikan bagaimana pemberitahuan muncul atas skrin anda +Comment[nb]=Tilpass visning av varsler på skjermen din +Comment[nl]=Weergave van meldingen aanpassen +Comment[oc]=Personalizar lo biais que las notificacions apareisson sus vòstre ecran +Comment[pa]=ਕਸਟਮਾਈਜ਼ ਕਰੋ ਕਿ ਤੁਹਾਡੀ ਸਕਰੀਨ ਉੱਤੇ ਨੋਟੀਫਿਕੇਸ਼ਨ ਕਿੰਝ ਵੇਖਾਈ ਦੇਣ +Comment[pl]=Konfiguruje ustwienia powiadamiania +Comment[pt]=Personalizar o aspeto das notificações no ecrã +Comment[pt_BR]=Personalize como as notificações devem aparecer na sua tela +Comment[ro]=Personalizați cum apar pe ecran notificările +Comment[ru]=Настройка отображения оповещений на вашем экране +Comment[sk]=Prispôsobiť spôsob upozornenia na obrazovke +Comment[sl]=Prilagodite prikaz obvestil na zaslonu +Comment[sq]=Përshtasni mënyrën se si shfaqen njoftimet në ekranin tuaj +Comment[sr]=Прилагодите начин приказа обавештења на екрану +Comment[sv]=Anpassa hur notifieringar ska visas på din skärm +Comment[th]=กำหนดหน้าตาของการแจ้งเหตุบนหน้าจอของคุณ +Comment[tr]=Bildirilerin görünümünü özelleştirin +Comment[ug]=ئۇقتۇرۇشلارنىڭ كۆرۈنۈش ئۇسۇلىنى ئۆزلەشتۈر +Comment[uk]=Налаштуйте показ сповіщень на Вашому екрані +Comment[vi]=Tùy chỉnh cách thông báo ẩn trên màn hình +Comment[zh_CN]=自定义通知在您屏幕上的显示方式 +Comment[zh_TW]=自訂通知該如何在您螢幕上顯示 +Exec=/usr/lib/mkbackup-btrfs/mkbackup-desktop-notifications.py +TryExec=/usr/lib/mkbackup-btrfs/mkbackup-desktop-notifications.py +Icon=xfce4-notifyd +Terminal=false +StartupNotify=false +Type=Application +Categories=GTK;Settings;DesktopSettings; diff --git a/register b/register new file mode 100644 index 0000000..e69de29