From 0bc3e51091084cc8224da4b5042e1a9e271b3de1 Mon Sep 17 00:00:00 2001 From: Wolfgang Link Date: Wed, 6 May 2015 11:45:10 +0200 Subject: [PATCH] Initial Project --- Makefile | 79 ++++ changelog.Debian | 6 + control.in | 9 + copyright | 16 + pve-zsync | 1150 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1260 insertions(+) create mode 100644 Makefile create mode 100644 changelog.Debian create mode 100644 control.in create mode 100644 copyright create mode 100644 pve-zsync diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f0b8bb2 --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ +RELEASE=1.0 + +VERSION=1.0 +PACKAGE=pve-zsync +PKGREL=1 + +DESTDIR= +PREFIX=/usr +BINDIR=${PREFIX}/bin +SBINDIR=${PREFIX}/sbin +MANDIR=${PREFIX}/share/man +DOCDIR=${PREFIX}/share/doc/${PACKAGE} +PODDIR=${DOCDIR}/pod +MAN1DIR=${MANDIR}/man8/ + +#ARCH:=$(shell dpkg-architecture -qDEB_BUILD_ARCH) +ARCH=all +GITVERSION:=$(shell cat .git/refs/heads/master) + +DEB=${PACKAGE}_${VERSION}-${PKGREL}_${ARCH}.deb + +all: ${DEB} + +.PHONY: dinstall +dinstall: deb + dpkg -i ${DEB} + +%.8.gz: %.8.man + rm -f $@ + gzip pve-zsync.8.man -c9 >$@ + +pve-zsync.8.man: pve-zsync + pod2man -c "Proxmox Documentation" -s 8 -r ${RELEASE} -n pve-zsync pve-zsync pve-zsync.8.man + +.PHONY: install +install: pve-zsync.8.man pve-zsync.8.gz + install -d ${DESTDIR}${SBINDIR} + install -m 0755 pve-zsync ${DESTDIR}${SBINDIR} + install -d ${DESTDIR}/usr/share/man/man8 + install -d ${DESTDIR}${PODDIR} + install -m 0644 pve-zsync.8.gz ${DESTDIR}/usr/share/man/man8/ + +.PHONY: deb ${DEB} +deb ${DEB}: + rm -rf debian + mkdir debian + install -d debian/var/lib/pve-zsync + install -d debian/etc/cron.d/ + echo "SHELL=/bin/sh" >> debian/etc/cron.d/pve-zsync + echo "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin" >> debian/etc/cron.d/pve-zsync + make DESTDIR=${CURDIR}/debian install + install -d -m 0755 debian/DEBIAN + sed -e s/@@VERSION@@/${VERSION}/ -e s/@@PKGRELEASE@@/${PKGREL}/ -e s/@@ARCH@@/${ARCH}/ debian/DEBIAN/control + install -D -m 0644 copyright debian/${DOCDIR}/copyright + install -m 0644 changelog.Debian debian/${DOCDIR}/ + gzip -9 debian/${DOCDIR}/changelog.Debian + echo "git clone git://git.proxmox.com/git/pve-storage.git\\ngit checkout ${GITVERSION}" > debian/${DOCDIR}/SOURCE + dpkg-deb --build debian + mv debian.deb ${DEB} + rm -rf debian + +.PHONY: clean +clean: + rm -rf debian *.deb ${PACKAGE}-*.tar.gz dist *.8.man *.8.gz + find . -name '*~' -exec rm {} ';' + +.PHONY: distclean +distclean: clean + + +.PHONY: upload +upload: ${DEB} + umount /pve/${RELEASE}; mount /pve/${RELEASE} -o rw + mkdir -p /pve/${RELEASE}/extra + rm -f /pve/${RELEASE}/extra/${PACKAGE}_*.deb + rm -f /pve/${RELEASE}/extra/Packages* + cp ${DEB} /pve/${RELEASE}/extra + cd /pve/${RELEASE}/extra; dpkg-scanpackages . /dev/null > Packages; gzip -9c Packages > Packages.gz + umount /pve/${RELEASE}; mount /pve/${RELEASE} -o ro diff --git a/changelog.Debian b/changelog.Debian new file mode 100644 index 0000000..60db576 --- /dev/null +++ b/changelog.Debian @@ -0,0 +1,6 @@ +libpve-pvesync-perl (0.0-1) unstable; urgency=low + + * initial package + + -- Proxmox Support Team So, 8 Mar 2015 18:34:19 +0100 + diff --git a/control.in b/control.in new file mode 100644 index 0000000..895896b --- /dev/null +++ b/control.in @@ -0,0 +1,9 @@ +Package: pve-zsync-perl +Version: @@VERSION@@-@@PKGRELEASE@@ +Section: perl +Priority: optional +Architecture: @@ARCH@@ +Depends: perl (>= 5.6.0-16) +Maintainer: Proxmox Support Team +Description: Proxmox VE storage management library + This package contains the Proxmox VE ZFS sync Tool. diff --git a/copyright b/copyright new file mode 100644 index 0000000..f96f3fb --- /dev/null +++ b/copyright @@ -0,0 +1,16 @@ +Copyright (C) 2010 Proxmox Server Solutions GmbH + +This software is written by Proxmox Server Solutions GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/pve-zsync b/pve-zsync new file mode 100644 index 0000000..b187e6c --- /dev/null +++ b/pve-zsync @@ -0,0 +1,1150 @@ +#!/usr/bin/perl + +my $PROGNAME = "pve-zsync"; +my $CONFIG_PATH = '/var/lib/'.$PROGNAME.'/'; +my $CONFIG = "$PROGNAME.cfg"; +my $CRONJOBS = '/etc/cron.d/'.$PROGNAME; +my $VMCONFIG = '/var/lib/'.$PROGNAME.'/'; +my $PATH = "/usr/sbin/"; +my $QEMU_CONF = '/etc/pve/local/qemu-server/'; +my $DEBUG = 0; + +use strict; +use warnings; +use Data::Dumper qw(Dumper); +use Fcntl qw(:flock SEEK_END); +use Getopt::Long; +use Switch; + +check_bin ('cstream'); +check_bin ('zfs'); +check_bin ('ssh'); +check_bin ('scp'); + +sub check_bin { + my ($bin) = @_; + + foreach my $p (split (/:/, $ENV{PATH})) { + my $fn = "$p/$bin"; + if (-x $fn) { + return $fn; + } + } + + warn "unable to find command '$bin'\n"; +} + +sub cut_to_width { + my ($text, $max) = @_; + + return $text if (length($text) <= $max); + my @spl = split('/', $text); + + my $count = length($spl[@spl-1]); + return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max; + + $count += length($spl[0]) if @spl > 1; + return substr($spl[0], 0, $max-4-length($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max; + + my $rest = 1 ; + $rest = $max-$count if ($max-$count > 0); + + return "$spl[0]".substr($text, length($spl[0]), $rest)."..\/".$spl[@spl-1]; +} + +sub lock { + my ($fh) = @_; + flock($fh, LOCK_EX) or die "Cannot lock config - $!\n"; + + seek($fh, 0, SEEK_END) or die "Cannot seek - $!\n"; +} + +sub unlock { + my ($fh) = @_; + flock($fh, LOCK_UN) or die "Cannot unlock config- $!\n"; +} + +sub check_config { + my ($source, $name, $cfg) = @_; + + if ($source->{vmid} && $cfg->{$source->{vmid}}->{$name}->{locked}){ + return "active" if $cfg->{$source->{vmid}}->{$name}->{locked} eq 'yes'; + return "exist" if $cfg->{$source->{vmid}}->{$name}->{locked} eq 'no'; + } elsif ($cfg->{$source->{abs_path}}->{$name}->{locked}) { + return "active" if $cfg->{$source->{abs_path}}->{$name}->{locked} eq 'yes'; + return "exist" if $cfg->{$source->{abs_path}}->{$name}->{locked} eq 'no'; + } + + return undef; +} + + +sub check_pool_exsits { + my ($pool, $ip) = @_; + + my $cmd = ''; + $cmd = "ssh root\@$ip " if $ip; + $cmd .= "zfs list $pool -H"; + eval { + run_cmd($cmd); + }; + + if($@){ + return 1; + } + return undef; +} + +sub write_to_config { + my ($cfg) = @_; + + open(my $fh, ">", "$CONFIG_PATH$CONFIG") + or die "cannot open >$CONFIG_PATH$CONFIG: $!\n"; + + my $text = decode_config($cfg); + + print($fh $text); + + close($fh); +} + +sub read_from_config { + + unless(-e "$CONFIG_PATH$CONFIG") { + return undef; + } + + open(my $fh, "<", "$CONFIG_PATH$CONFIG") + or die "cannot open > $CONFIG_PATH$CONFIG: $!\n"; + + $/ = undef; + + my $text = <$fh>; + + unlock($fh); + + my $cfg = encode_config($text); + + return $cfg; +} + +sub decode_config { + my ($cfg) = @_; + my $raw = ''; + foreach my $source (keys%{$cfg}){ + foreach my $sync_name (keys%{$cfg->{$source}}){ + $raw .= "$source: $sync_name\n"; + foreach my $parameter (keys%{$cfg->{$source}->{$sync_name}}){ + $raw .= "\t$parameter: $cfg->{$source}->{$sync_name}->{$parameter}\n"; + } + } + } + return $raw; +} + +sub encode_config { + my ($raw) = @_; + my $cfg = {}; + my $source; + my $check = 0; + my $sync_name; + + while ($raw && $raw =~ s/^(.*?)(\n|$)//) { + my $line = $1; + + next if $line =~ m/^\#/; + next if $line =~ m/^\s*$/; + + if ($line =~ m/^(\t| )(\w+): (.+)/){ + my $par = $2; + my $value = $3; + + if ($par eq 'source_pool') { + $cfg->{$source}->{$sync_name}->{$par} = $value; + die "error in Config: SourcePool value doubled\n" if ($check & 1); + $check += 1; + } elsif ($par eq 'source_ip') { + $cfg->{$source}->{$sync_name}->{$par} = $value; + die "error in Config: SourceIP value doubled\n" if ($check & 2); + $check += 2; + } elsif ($par eq 'locked') { + $cfg->{$source}->{$sync_name}->{$par} = $value; + die "error in Config: Locked value doubled\n" if ($check & 4); + $check += 4; + } elsif ($par eq 'method') { + $cfg -> {$source}->{$sync_name}->{$par} = $value; + die "error in Config: Method value doubled\n" if ($check & 8); + $check += 8; + } elsif ($par eq 'interval') { + $cfg -> {$source}->{$sync_name}->{$par} = $value; + die "error in Config: Iterval value doubled\n" if ($check & 16); + $check += 16; + } elsif ($par eq 'limit') { + $cfg -> {$source}->{$sync_name}->{$par} = $value; + die "error in Config: Limit value doubled\n" if ($check & 32); + $check += 32; + } elsif ($par eq 'dest_pool') { + $cfg -> {$source}->{$sync_name}->{$par} = $value; + die "error in Config: DestPool value doubled\n" if ($check & 64); + $check += 64; + } elsif ($par eq 'dest_ip') { + $cfg -> {$source}->{$sync_name}->{$par} = $value; + die "error in Config: DestIp value doubled\n" if ($check & 128); + $check += 128; + } elsif ($par eq 'dest_path') { + $cfg -> {$source}->{$sync_name}->{$par} = $value; + die "error in Config: DestPath value doubled\n" if ($check & 256); + $check += 256; + } elsif ($par eq 'source_path') { + $cfg -> {$source}->{$sync_name}->{$par} = $value; + die "error in Config: SourcePath value doubled\n" if ($check & 512); + $check += 512; + } elsif ($par eq 'vmid') { + $cfg -> {$source}->{$sync_name}->{$par} = $value; + die "error in Config: Vmid value doubled\n" if ($check & 1024); + $check += 1024; + } elsif ($par =~ 'lsync') { + $cfg->{$source}->{$sync_name}->{$par} = $value; + die "error in Config: lsync value doubled\n" if ($check & 2048); + $check += 2048; + } elsif ($par =~ 'maxsnap') { + $cfg->{$source}->{$sync_name}->{$par} = $value; + die "error in Config: maxsnap value doubled\n" if ($check & 4096); + $check += 4096; + } else { + die "error in Config\n"; + } + } elsif ($line =~ m/^((\d+.\d+.\d+.\d+):)?([\w\-\_\/]+): (.+){0,1}/){ + $source = $3; + $sync_name = $4 ? $4 : 'default' ; + $cfg->{$source}->{$sync_name} = undef; + $cfg->{$source}->{$sync_name}->{source_ip} = $2 if $2; + $check = 0; + } + } + return $cfg; +} + +sub parse_target { + my ($text) = @_; + + if ($text =~ m/^((\d+.\d+.\d+.\d+):)?((\w+)\/?)([\w\/\-\_]*)?$/) { + + die "Input not valid\n" if !$3; + my $tmp = $3; + my $target = {}; + + if ($2) { + $target->{ip} = $2 ; + } + + if ($tmp =~ m/^(\d\d\d+)$/){ + $target->{vmid} = $tmp; + } else { + $target->{pool} = $4; + my $abs_path = $4; + if ($5) { + $target->{path} = "\/$5"; + $abs_path .= "\/$5"; + } + $target->{abs_path} = $abs_path; + } + + return $target; + } + die "Input not valid\n"; +} + +sub list { + + my $cfg = read_from_config("$CONFIG_PATH$CONFIG"); + + my $list = sprintf("%-25s%-15s%-7s%-20s%-10s%-5s\n" , "SOURCE", "NAME", "ACTIVE", "LAST SYNC", "INTERVAL", "TYPE"); + + foreach my $source (keys%{$cfg}){ + foreach my $sync_name (keys%{$cfg->{$source}}){ + my $source_name = $source; + $source_name = $cfg->{$source}->{$sync_name}->{source_ip}.":".$source if $cfg->{$source}->{$sync_name}->{source_ip}; + $list .= sprintf("%-25s%-15s", cut_to_width($source_name,25), cut_to_width($sync_name,15)); + $list .= sprintf("%-7s",$cfg->{$source}->{$sync_name}->{locked}); + $list .= sprintf("%-20s",$cfg->{$source}->{$sync_name}->{lsync}); + $list .= sprintf("%-10s",$cfg->{$source}->{$sync_name}->{interval}); + $list .= sprintf("%-5s\n",$cfg->{$source}->{$sync_name}->{method}); + } + } + + return $list; +} + +sub vm_exists { + my ($target) = @_; + + my $cmd = ""; + $cmd = "ssh root\@$target->{ip} " if ($target->{ip}); + $cmd .= "qm status $target->{vmid}"; + + my $res = run_cmd($cmd); + + return 1 if ($res =~ m/^status.*$/); + return undef; +} + +sub init { + my ($param) = @_; + + my $cfg = read_from_config; + + my $vm = {}; + + my $name = $param->{name} ? $param->{name} : "default"; + my $interval = $param->{interval} ? $param->{interval} : 15; + + my $source = parse_target($param->{source}); + my $dest = parse_target($param->{dest}); + + $vm->{$name}->{dest_pool} = $dest->{pool}; + $vm->{$name}->{dest_ip} = $dest->{ip} if $dest->{ip}; + $vm->{$name}->{dest_path} = $dest->{path} if $dest->{path}; + + $param->{method} = "local" if !$dest->{ip} && !$source->{ip}; + $vm->{$name}->{locked} = "no"; + $vm->{$name}->{interval} = $interval; + $vm->{$name}->{method} = $param->{method} ? $param->{method} : "ssh"; + $vm->{$name}->{limit} = $param->{limit} if $param->{limit}; + $vm->{$name}->{maxsnap} = $param->{maxsnap} if $param->{maxsnap}; + + if ( my $ip = $vm->{$name}->{dest_ip} ) { + run_cmd("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip"); + } + + if ( my $ip = $source->{ip} ) { + run_cmd("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$ip"); + } + + die "Pool $dest->{abs_path} does not exists\n" if check_pool_exsits($dest->{abs_path}, $dest->{ip}); + + my $check = check_pool_exsits($source->{abs_path}, $source->{ip}) if !$source->{vmid} && $source->{abs_path}; + + die "Pool $source->{abs_path} does not exists\n" if undef($check); + + my $add_job = sub { + my ($vm, $name) = @_; + my $source = ""; + + if ($vm->{$name}->{vmid}) { + $source = "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip}; + $source .= $vm->{$name}->{vmid}; + } else { + $source = $vm->{$name}->{source_pool}; + $source .= $vm->{$name}->{source_path} if $vm->{$name}->{source_path}; + } + die "Config already exists\n" if $cfg->{$source}->{$name}; + + cron_add($vm); + + $cfg->{$source}->{$name} = $vm->{$name}; + + write_to_config($cfg); + }; + + if ($source->{vmid}) { + die "VM $source->{vmid} doesn't exist\n" if !vm_exists($source); + my $disks = get_disks($source); + $vm->{$name}->{vmid} = $source->{vmid}; + $vm->{$name}->{lsync} = 0; + $vm->{$name}->{source_ip} = $source->{ip} if $source->{ip}; + + &$add_job($vm, $name); + + } else { + $vm->{$name}->{source_pool} = $source->{pool}; + $vm->{$name}->{source_ip} = $source->{ip} if $source->{ip}; + $vm->{$name}->{source_path} = $source->{path} if $source->{path}; + $vm->{$name}->{lsync} = 0; + + &$add_job($vm, $name); + } + + sync($param) if !$param->{skip}; +} + +sub destroy { + my ($param) = @_; + + my $cfg = read_from_config("$CONFIG_PATH$CONFIG"); + my $name = $param->{name} ? $param->{name} : "default"; + + my $source = parse_target($param->{source}); + + my $delete_cron = sub { + my ($path, $name, $cfg) = @_; + + die "Source does not exist!\n" unless $cfg->{$path} ; + + die "Sync Name does not exist!\n" unless $cfg->{$path}->{$name}; + + delete $cfg->{$path}->{$name}; + + delete $cfg->{$path} if keys%{$cfg->{$path}} == 0; + + write_to_config($cfg); + + cron_del($path, $name); + }; + + + if ($source->{vmid}) { + my $path = $source->{vmid}; + + &$delete_cron($path, $name, $cfg) + + } else { + + my $path = $source->{pool}; + $path .= $source->{path} if $source->{path}; + + &$delete_cron($path, $name, $cfg); + } +} + +sub sync { + my ($param) = @_; + + my $cfg = read_from_config("$CONFIG_PATH$CONFIG"); + + my $name = $param->{name} ? $param->{name} : "default"; + my $max_snap = $param->{maxsnap} ? $param->{maxsnap} : 1; + my $method = $param->{method} ? $param->{method} : "ssh"; + + my $dest = parse_target($param->{dest}); + my $source = parse_target($param->{source}); + + my $sync_path = sub { + my ($source, $name, $cfg, $max_snap, $dest, $method) = @_; + + ($source->{old_snap},$source->{last_snap}) = snapshot_get($source, $dest, $max_snap, $name); + + my $job_status = check_config($source, $name, $cfg) if $cfg; + die "VM syncing at the moment!\n" if ($job_status && $job_status eq "active"); + + if ($job_status && $job_status eq "exist") { + my $conf_name = $source->{abs_path}; + $conf_name = $source->{vmid} if $source->{vmid}; + $cfg->{$conf_name}->{$name}->{locked} = "yes"; + write_to_config($cfg); + } + + my $date = snapshot_add($source, $dest, $name); + + send_image($source, $dest, $method, $param->{verbose}, $param->{limit}); + + snapshot_destroy($source, $dest, $method, $source->{old_snap}) if ($source->{destroy} && $source->{old_snap}); + + if ($job_status && $job_status eq "exist") { + my $conf_name = $source->{abs_path}; + $conf_name = $source->{vmid} if $source->{vmid}; + $cfg->{$conf_name}->{$name}->{locked} = "no"; + $cfg->{$conf_name}->{$name}->{lsync} = $date; + write_to_config($cfg); + } + }; + + $param->{method} = "ssh" if !$param->{method}; + + if ($source->{vmid}) { + die "VM $source->{vmid} doesn't exist\n" if !vm_exists($source); + my $disks = get_disks($source); + + foreach my $disk (keys %{$disks}) { + $source->{abs_path} = $disks->{$disk}->{pool}; + $source->{abs_path} .= "\/$disks->{$disk}->{path}" if $disks->{$disk}->{path}; + + $source->{pool} = $disks->{$disk}->{pool}; + $source->{path} = "\/$disks->{$disk}->{path}"; + + &$sync_path($source, $name, $cfg, $max_snap, $dest, $method); + } + } else { + &$sync_path($source, $name, $cfg, $max_snap, $dest, $method); + } +} + +sub snapshot_get{ + my ($source, $dest, $max_snap, $name) = @_; + + my $cmd = "zfs list -r -t snapshot -Ho name, -S creation "; + + $cmd .= $source->{abs_path}; + $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip}; + + my $raw = run_cmd($cmd); + my $index = 1; + my $line = ""; + my $last_snap = undef; + + while ($raw && $raw =~ s/^(.*?)(\n|$)//) { + $line = $1; + $last_snap = $line if $index == 1; + if ($index == $max_snap) { + $source->{destroy} = 1; + last; + }; + $index++; + } + + $line =~ m/^(.+)\@(rep_$name\_.+)(\n|$)/; + return ($2, $last_snap) if $2; + + return undef; +} + +sub snapshot_add { + my ($source, $dest, $name) = @_; + + my $date = get_date(); + + my $snap_name = "rep_$name\_".$date; + + $source->{new_snap} = $snap_name; + + my $path = $source->{abs_path}."\@".$snap_name; + + my $cmd = "zfs snapshot $path"; + $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip}; + + eval{ + run_cmd($cmd); + }; + + if (my $err = $@){ + snapshot_destroy($source, $dest, 'ssh', $snap_name); + die "$err\n"; + } + return $date; +} + +sub cron_add { + my ($vm) = @_; + + open(my $fh, '>>', "$CRONJOBS") + or die "Could not open file: $!\n"; + + foreach my $name (keys%{$vm}){ + my $text = "*/$vm->{$name}->{interval} * * * * root "; + $text .= "$PATH$PROGNAME sync"; + $text .= " -source "; + if ($vm->{$name}->{vmid}) { + $text .= "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip}; + $text .= "$vm->{$name}->{vmid} "; + } else { + $text .= "$vm->{$name}->{source_ip}:" if $vm->{$name}->{source_ip}; + $text .= "$vm->{$name}->{source_pool}"; + $text .= "$vm->{$name}->{source_path}" if $vm->{$name}->{source_path}; + } + $text .= " -dest "; + $text .= "$vm->{$name}->{dest_ip}:" if $vm->{$name}->{dest_ip}; + $text .= "$vm->{$name}->{dest_pool}"; + $text .= "$vm->{$name}->{dest_path}" if $vm->{$name}->{dest_path}; + $text .= " -name $name "; + $text .= " -limit $vm->{$name}->{limit}" if $vm->{$name}->{limit}; + $text .= " -maxsnap $vm->{$name}->{maxsnap}" if $vm->{$name}->{maxsnap}; + $text .= "\n"; + print($fh $text); + } + close($fh); +} + +sub cron_del { + my ($source, $name) = @_; + + open(my $fh, '<', "$CRONJOBS") + or die "Could not open file: $!\n"; + + $/ = undef; + + my $text = <$fh>; + my $buffer = ""; + close($fh); + while ($text && $text =~ s/^(.*?)(\n|$)//) { + my $line = $1.$2; + if ($line !~ m/.*$PROGNAME.*$source.*$name.*/){ + $buffer .= $line; + } + } + open($fh, '>', "$CRONJOBS") + or die "Could not open file: $!\n"; + print($fh $buffer); + close($fh); +} + +sub get_disks { + my ($target) = @_; + + my $cmd = ""; + $cmd = "ssh root\@$target->{ip} " if $target->{ip}; + $cmd .= "qm config $target->{vmid}"; + + my $res = run_cmd($cmd); + + my $disks = parse_disks($res, $target->{ip}); + + return $disks; +} + +sub run_cmd { + my ($cmd) = @_; + print "Start CMD\n" if $DEBUG; + print Dumper $cmd if $DEBUG; + my $output = `$cmd 2>&1`; + + die $output if 0 != $?; + + chomp($output); + print Dumper $output if $DEBUG; + print "END CMD\n" if $DEBUG; + return $output; +} + +sub parse_disks { + my ($text, $ip) = @_; + + my $disks; + + my $num = 0; + my $cmd = ""; + $cmd .= "ssh root\@$ip " if $ip; + $cmd .= "pvesm zfsscan"; + my $zfs_pools = run_cmd($cmd); + while ($text && $text =~ s/^(.*?)(\n|$)//) { + my $line = $1; + my $disk = undef; + my $stor = undef; + if($line =~ m/^(virtio\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) { + $disk = $3; + $stor = $2; + } elsif($line =~ m/^(ide\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) { + $disk = $3; + $stor = $2; + } elsif($line =~ m/^(scsi\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) { + $disk = $3; + $stor = $2; + } elsif($line =~ m/^(sata\d+: )(.+:)([A-Za-z0-9\-]+),(.*)$/) { + $disk = $3; + $stor = $2; + } + + if($disk && $disk ne "none" && $disk !~ m/cdrom/ ) { + my $cmd = ""; + $cmd .= "ssh root\@$ip " if $ip; + $cmd .= "pvesm path $stor$disk"; + my $path = run_cmd($cmd); + + if ($path =~ m/^\/dev\/zvol\/(\w+).*(\/$disk)$/){ + + $disks->{$num}->{pool} = $1; + $disks->{$num}->{path} = $disk; + $num++; + + } else { + die "ERROR: in path\n"; + } + } + } + die "disk is not on ZFS Storage\n" if $num == 0; + return $disks; +} + +sub snapshot_destroy { + my ($source, $dest, $method, $snap) = @_; + + my $zfscmd = "zfs destroy "; + my $name = "$source->{path}\@$snap"; + + eval { + if($source->{ip} && $method eq 'ssh'){ + run_cmd("ssh root\@$source->{ip} $zfscmd $source->{pool}$name"); + } else { + run_cmd("$zfscmd $source->{pool}$name"); + } + }; + if (my $erro = $@) { + warn "WARN: $erro"; + } + if ($dest){ + my $ssh = $dest->{ip} ? "ssh root\@$dest->{ip}" : ""; + + my $path = ""; + $path ="$dest->{path}" if $dest->{path}; + + my @dir = split(/\//, $source->{path}); + eval { + run_cmd("$ssh $zfscmd $dest->{pool}$path\/$dir[@dir-1]\@$snap "); + }; + if (my $erro = $@) { + warn "WARN: $erro"; + } + } +} + +sub snapshot_exist { + my ($source ,$dest, $method) = @_; + + my $cmd = ""; + $cmd = "ssh root\@$dest->{ip} " if $dest->{ip}; + $cmd .= "zfs list -rt snapshot -Ho name $dest->{pool}"; + $cmd .= "$dest->{path}" if $dest->{path}; + my @dir = split(/\//, $source->{path}); + $cmd .= "\/$dir[@dir-1]\@$source->{old_snap}"; + + my $text = ""; + eval {$text =run_cmd($cmd);}; + if (my $erro = $@) { + warn "WARN: $erro"; + return undef; + } + + while ($text && $text =~ s/^(.*?)(\n|$)//) { + my $line = $1; + return 1 if $line =~ m/^.*$source->{old_snap}$/; + } +} + +sub send_image { + my ($source, $dest, $method, $verbose, $limit) = @_; + + my $cmd = ""; + + $cmd .= "ssh root\@$source->{ip} " if $source->{ip}; + $cmd .= "zfs send "; + $cmd .= "-v " if $verbose; + + if($source->{last_snap} && snapshot_exist($source ,$dest, $method)) { + $cmd .= "-i $source->{abs_path}\@$source->{old_snap} $source->{abs_path}\@$source->{new_snap} "; + } else { + $cmd .= "$source->{abs_path}\@$source->{new_snap} "; + } + + if ($limit){ + my $bwl = $limit*1024; + $cmd .= "| cstream -t $bwl"; + } + $cmd .= "| "; + $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip}; + $cmd .= "zfs recv $dest->{pool}"; + $cmd .= "$dest->{path}" if $dest->{path}; + + my @dir = split(/\//,$source->{path}); + $cmd .= "\/$dir[@dir-1]\@$source->{new_snap}"; + + eval { + run_cmd($cmd) + }; + + if (my $erro = $@) { + snapshot_destroy($source, undef, $method, $source->{new_snap}); + die $erro; + }; + + if ($source->{vmid}) { + if ($method eq "ssh") { + send_config($source, $dest,'ssh'); + } + } +} + + +sub send_config{ + my ($source, $dest, $method) = @_; + + if ($method eq 'ssh'){ + if ($dest->{ip} && $source->{ip}) { + run_cmd("ssh root\@$dest->{ip} mkdir $VMCONFIG -p"); + run_cmd("scp root\@$source->{ip}:$QEMU_CONF$source->{vmid}.conf root\@$dest->{ip}:$VMCONFIG$source->{vmid}.conf.$source->{new_snap}"); + } elsif ($dest->{ip}) { + run_cmd("ssh root\@$dest->{ip} mkdir $VMCONFIG -p"); + run_cmd("scp $QEMU_CONF$source->{vmid}.conf root\@$dest->{ip}:$VMCONFIG$source->{vmid}.conf.$source->{new_snap}"); + } elsif ($source->{ip}) { + run_cmd("mkdir $VMCONFIG -p"); + run_cmd("scp root\@$source->{ip}:$QEMU_CONF$source->{vmid}.conf $VMCONFIG$source->{vmid}.conf.$source->{new_snap}"); + } + + if ($source->{destroy}){ + if($dest->{ip}){ + run_cmd("ssh root\@$dest->{ip} rm -f $VMCONFIG$source->{vmid}.conf.$source->{old_snap}"); + } else { + run_cmd("rm -f $VMCONFIG$source->{vmid}.conf.$source->{old_snap}"); + } + } + } +} + +sub get_date { + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + my $datestamp = sprintf ( "%04d-%02d-%02d_%02d:%02d:%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec); + + return $datestamp; +} + +sub status { + my $cfg = read_from_config("$CONFIG_PATH$CONFIG"); + + my $status_list = sprintf("%-25s%-15s%-10s\n","SOURCE","NAME","STATUS"); + + foreach my $source (keys%{$cfg}){ + foreach my $sync_name (keys%{$cfg->{$source}}){ + my $status; + + my $source_name = $source; + + $source_name = $cfg->{$source}->{$sync_name}->{source_ip}.":".$source if $cfg->{$source}->{$sync_name}->{source_ip}; + + if ($cfg->{$source}->{$sync_name}->{locked} eq 'no'){ + $status = sprintf("%-10s","OK"); + } elsif ($cfg->{$source}->{$sync_name}->{locked} eq 'yes' && + $cfg->{$source}->{$sync_name}->{failure}) { + $status = sprintf("%-10s","sync error"); + } else { + $status = sprintf("%-10s","syncing"); + } + + $status_list .= sprintf("%-25s%-15s", cut_to_width($source_name,25), cut_to_width($sync_name,15)); + $status_list .= "$status\n"; + } + } + + return $status_list; +} + + +my $command = $ARGV[0]; + +my $commands = {'destroy' => 1, + 'create' => 1, + 'sync' => 1, + 'list' => 1, + 'status' => 1, + 'help' => 1}; + +if (!$command || !$commands->{$command}) { + usage(); + die "\n"; +} + +my $dest = ''; +my $source = ''; +my $verbose = ''; +my $interval = ''; +my $limit = ''; +my $maxsnap = ''; +my $name = ''; +my $skip = ''; + +my $help_sync = "zfs-zsync sync -dest -source [OPTIONS]\n +\twill sync one time\n +\t-dest\tstring\n +\t\tthe destination target is like [IP:][/Path]\n +\t-limit\tinteger\n +\t\tmax sync speed in kBytes/s, default unlimited\n +\t-maxsnap\tinteger\n +\t\thow much snapshots will be kept before get erased, default 1/n +\t-name\tstring\n +\t\tname of the sync job, if not set it is default. +\tIt is only necessary if scheduler allready contains this source.\n +\t-source\tstring\n +\t\tthe source can be an or [IP:][/Path]\n"; + +my $help_create = "zfs-zsync create -dest -source [OPTIONS]/n +\tCreate a sync Job\n +\t-dest\tstringn\n +\t\tthe destination target is like [IP]:[/Path]\n +\t-interval\tinteger\n +\t\tthe interval in min in witch the zfs will sync, +\t\tdefault is 15 min\n +\t-limit\tinteger\n +\t\tmax sync speed, default unlimited\n +\t-maxsnap\tstring\n +\t\thow much snapshots will be kept before get erased, default 1\n +\t-name\tstring\n +\t\tname of the sync job, if not set it is default\n +\t-skip\tboolean\n +\t\tif this flag is set it will skip the first sync\n +\t-source\tstring\n +\t\tthe source can be an or [IP:][/Path]\n"; + +my $help_destroy = "zfs-zsync destroy -source [OPTIONS]\n +\tremove a sync Job from the scheduler\n +\t-name\tstring\n +\t\tname of the sync job, if not set it is default\n +\t-source\tstring\n +\t\tthe source can be an or [IP:][/Path]\n"; + +my $help_help = "zfs-zsync help [OPTIONS]\n +\tGet help about specified command.\n +\t\tstring\n +\t\tCommand name\n +\t-verbose\tboolean\n +\t\tVerbose output format.\n"; + +my $help_list = "zfs-zsync list\n +\tGet a List of all scheduled Sync Jobs\n"; + +my $help_status = "zfs-zsync status\n +\tGet the status of all scheduled Sync Jobs\n"; + +sub help{ + my ($command) = @_; + + switch($command){ + case 'help' + { + die "$help_help\n"; + } + case 'sync' + { + die "$help_sync\n"; + } + case 'destroy' + { + die "$help_destroy\n"; + } + case 'create' + { + die "$help_create\n"; + } + case 'list' + { + die "$help_list\n"; + } + case 'status' + { + die "$help_status\n"; + } + } + +} + +my $err = GetOptions ('dest=s' => \$dest, + 'source=s' => \$source, + 'verbose' => \$verbose, + 'interval=i' => \$interval, + 'limit=i' => \$limit, + 'maxsnap=i' => \$maxsnap, + 'name=s' => \$name, + 'skip' => \$skip); + +if ($err == 0) { + die "can't parse options\n"; +} + +my $param; +$param->{dest} = $dest; +$param->{source} = $source; +$param->{verbose} = $verbose; +$param->{interval} = $interval; +$param->{limit} = $limit; +$param->{maxsnap} = $maxsnap; +$param->{name} = $name; +$param->{skip} = $skip; + +switch($command){ + case "destroy" + { + die "$help_destroy\n" if !$source; + check_target($source); + destroy($param); + } + case "sync" + { + die "$help_sync\n" if !$source || !$dest; + check_target($source); + check_target($dest); + sync($param); + } + case "create" + { + die "$help_create\n" if !$source || !$dest; + check_target($source); + check_target($dest); + init($param); + } + case "status" + { + print status(); + } + case "list" + { + print list(); + } + case "help" + { + my $help_command = $ARGV[1]; + if ($help_command && $commands->{$help_command}) { + print help($help_command); + } + if ($verbose){ + exec("man $PROGNAME"); + } else { + usage(1); + } + } +} + +sub usage{ + my ($help) = @_; + + print("ERROR:\tno command specified\n") if !$help; + print("USAGE:\t$PROGNAME [ARGS] [OPTIONS]\n"); + print("\tpve-zsync help [] [OPTIONS]\n\n"); + print("\tpve-zsync create -dest -source [OPTIONS]\n"); + print("\tpve-zsync destroy -source [OPTIONS]\n"); + print("\tpve-zsync list\n"); + print("\tpve-zsync status\n"); + print("\tpve-zsync sync -dest -source [OPTIONS]\n"); +} + +sub check_target{ + my ($target) = @_; + + chomp($target); + + if($target !~ m/(\d+.\d+.\d+.\d+:)?([\w\-\_\/]+)(\/.+)?/){ + print("ERROR:\t$target is not valid.\n\tUse [IP:][/Path]!\n"); + return 1; + } + return undef; +} + +__END__ + +=head1 NAME + +pve-zsync - PVE ZFS Replication Manager + +=head1 SYNOPSIS + +zfs-zsync [ARGS] [OPTIONS] + +zfs-zsync help [OPTIONS] + + Get help about specified command. + + string + + Command name + + -verbose boolean + + Verbose output format. + +zfs-zsync create -dest -source [OPTIONS] + + Create a sync Job + + -dest string + + the destination target is like [IP]:[/Path] + + -interval integer + + the interval in min in witch the zfs will sync, default is 15 min + + -limit integer + + max sync speed, default unlimited + + -maxsnap string + + how much snapshots will be kept before get erased, default 1 + + -name string + + name of the sync job, if not set it is default + + -skip boolean + + if this flag is set it will skip the first sync + + -source string + + the source can be an or [IP:][/Path] + +zfs-zsync destroy -source [OPTIONS] + + remove a sync Job from the scheduler + + -name string + + name of the sync job, if not set it is default + + -source string + + the source can be an or [IP:][/Path] + +zfs-zsync list + + Get a List of all scheduled Sync Jobs + +zfs-zsync status + + Get the status of all scheduled Sync Jobs + +zfs-zsync sync -dest -source [OPTIONS] + + will sync one time + + -dest string + + the destination target is like [IP:][/Path] + + -limit integer + + max sync speed in kBytes/s, default unlimited + + -maxsnap integer + + how much snapshots will be kept before get erased, default 1 + + -name string + + name of the sync job, if not set it is default. + It is only necessary if scheduler allready contains this source. + + -source string + + the source can be an or [IP:][/Path] + +=head1 DESCRIPTION + +This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers. +This tool also has the capability to add jobs to cron so the sync will be automatically done. + +=head2 PVE ZFS Storage sync Tool + +This Tool can get remote pool on other PVE or send Pool to others ZFS machines + +=head1 EXAMPLES + +add sync job from local VM to remote ZFS Server +zfs-zsync -source=100 -dest=192.168.1.2:zfspool + +=head1 IMPORTANT FILES + +Where the cron jobs are stored /etc/cron.d/pve-zsync +Where the VM config get copied on the destination machine /var/pve-zsync +Where the config is stored /var/pve-zsync + +Copyright (C) 2007-2015 Proxmox Server Solutions GmbH + +This program is free software: you can redistribute it and/or modify it +under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 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 +Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public +License along with this program. If not, see +. -- 2.39.2