X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=pve-zsync;h=bc06ae12cddfd433523306318275f8f5658dfddd;hb=1fe362eb85824f9d99f4fa946a485e359f688c4a;hp=cd2a22891bcb39e46282fbd925fb371130392a63;hpb=6b4f676d3b3e75ee81d2952bc7ba1913216059d8;p=pve-zsync.git diff --git a/pve-zsync b/pve-zsync index cd2a228..bc06ae1 100644 --- a/pve-zsync +++ b/pve-zsync @@ -6,33 +6,63 @@ use Data::Dumper qw(Dumper); use Fcntl qw(:flock SEEK_END); use Getopt::Long qw(GetOptionsFromArray); use File::Copy qw(move); -use Switch; +use File::Path qw(make_path); use JSON; use IO::File; +use String::ShellQuote 'shell_quote'; my $PROGNAME = "pve-zsync"; -my $CONFIG_PATH = "/var/lib/${PROGNAME}/"; -my $STATE = "${CONFIG_PATH}sync_state"; +my $CONFIG_PATH = "/var/lib/${PROGNAME}"; +my $STATE = "${CONFIG_PATH}/sync_state"; my $CRONJOBS = "/etc/cron.d/$PROGNAME"; -my $PATH = "/usr/sbin/"; -my $QEMU_CONF = "/etc/pve/local/qemu-server/"; -my $LOCKFILE = "$CONFIG_PATH${PROGNAME}.lock"; -my $PROG_PATH = "$PATH${PROGNAME}"; +my $PATH = "/usr/sbin"; +my $PVE_DIR = "/etc/pve/local"; +my $QEMU_CONF = "${PVE_DIR}/qemu-server"; +my $LXC_CONF = "${PVE_DIR}/lxc"; +my $LOCKFILE = "$CONFIG_PATH/${PROGNAME}.lock"; +my $PROG_PATH = "$PATH/${PROGNAME}"; my $INTERVAL = 15; my $DEBUG = 0; +my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])"; +my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)"; +my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})"; +my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))"; + +my $IPV6RE = "(?:" . + "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" . + "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" . + "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" . + "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" . + "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))"; + +my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address +my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too +my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets +# targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional +my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!; + check_bin ('cstream'); check_bin ('zfs'); check_bin ('ssh'); check_bin ('scp'); +$SIG{TERM} = $SIG{QUIT} = $SIG{PIPE} = $SIG{HUP} = $SIG{KILL} = $SIG{INT} = + sub { + die "Signal aborting sync\n"; + }; + sub check_bin { my ($bin) = @_; foreach my $p (split (/:/, $ENV{PATH})) { - my $fn = "$p/$bin"; - if (-x $fn) { - return $fn; + my $fn = "$p/$bin"; + if (-x $fn) { + return $fn; } } @@ -40,21 +70,33 @@ sub check_bin { } sub cut_target_width { - my ($target, $max) = @_; + my ($path, $maxlen) = @_; + $path =~ s@/+@/@g; - return $target if (length($target) <= $max); - my @spl = split('/', $target); + return $path if length($path) <= $maxlen; - my $count = length($spl[@spl-1]); - return "..\/".substr($spl[@spl-1],($count-$max)+3 ,$count) if $count > $max; + return '..'.substr($path, -$maxlen+2) if $path !~ m@/@; - $count += length($spl[0]) if @spl > 1; - return substr($spl[0], 0, $max-4-length($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max; + $path =~ s@/([^/]+/?)$@@; + my $tail = $1; - my $rest = 1; - $rest = $max-$count if ($max-$count > 0); + if (length($tail)+3 == $maxlen) { + return "../$tail"; + } elsif (length($tail)+2 >= $maxlen) { + return '..'.substr($tail, -$maxlen+2) + } + + $path =~ s@(/[^/]+)(?:/|$)@@; + my $head = $1; + my $both = length($head) + length($tail); + my $remaining = $maxlen-$both-4; # -4 for "/../" - return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1]; + if ($remaining < 0) { + return substr($head, 0, $maxlen - length($tail) - 3) . "../$tail"; # -3 for "../" + } + + substr($path, ($remaining/2), (length($path)-$remaining), '..'); + return "$head/" . $path . "/$tail"; } sub lock { @@ -77,20 +119,23 @@ sub get_status { return undef; } -sub check_pool_exsits { +sub check_pool_exists { my ($target) = @_; - my $cmd = ''; - $cmd = "ssh root\@$target->{ip} " if $target->{ip}; - $cmd .= "zfs list $target->{all} -H"; + my $cmd = []; + + if ($target->{ip}) { + push @$cmd, 'ssh', "root\@$target->{ip}", '--'; + } + push @$cmd, 'zfs', 'list', '-H', '--', $target->{all}; eval { run_cmd($cmd); }; if ($@) { - return 1; + return 0; } - return undef; + return 1; } sub parse_target { @@ -99,20 +144,20 @@ sub parse_target { my $errstr = "$text : is not a valid input! Use [IP:] or [IP:][/Path]"; my $target = {}; - $text =~ m/^((\d+.\d+.\d+.\d+):)?(.*)$/; - - $target->{all} = $3; - - if ($2) { - $target->{ip} = $2; + if ($text !~ $TARGETRE) { + die "$errstr\n"; } - my @parts = split('/', $3); + $target->{all} = $2; + $target->{ip} = $1 if $1; + my @parts = split('/', $2); + + $target->{ip} =~ s/^\[(.*)\]$/$1/ if $target->{ip}; - die "$text $errstr\n" if !($target->{pool} = shift(@parts)); - die "$text $errstr\n" if $target->{pool} =~ /^(\d+.\d+.\d+.\d+)$/; + my $pool = $target->{pool} = shift(@parts); + die "$errstr\n" if !$pool; - if ($target->{pool} =~ m/^\d+$/) { - $target->{vmid} = $target->{pool}; + if ($pool =~ m/^\d+$/) { + $target->{vmid} = $pool; delete $target->{pool}; } @@ -163,14 +208,14 @@ sub parse_argv { $param->{method} = undef; my ($ret, $ar) = GetOptionsFromArray(\@arg, - 'dest=s' => \$param->{dest}, - 'source=s' => \$param->{source}, - 'verbose' => \$param->{verbose}, - 'limit=i' => \$param->{limit}, - 'maxsnap=i' => \$param->{maxsnap}, - 'name=s' => \$param->{name}, - 'skip' => \$param->{skip}, - 'method=s' => \$param->{method}); + 'dest=s' => \$param->{dest}, + 'source=s' => \$param->{source}, + 'verbose' => \$param->{verbose}, + 'limit=i' => \$param->{limit}, + 'maxsnap=i' => \$param->{maxsnap}, + 'name=s' => \$param->{name}, + 'skip' => \$param->{skip}, + 'method=s' => \$param->{method}); if ($ret == 0) { die "can't parse options\n"; @@ -191,6 +236,7 @@ sub add_state_to_job { $job->{state} = $state->{state}; $job->{lsync} = $state->{lsync}; + $job->{vm_type} = $state->{vm_type}; for (my $i = 0; $state->{"snap$i"}; $i++) { $job->{"snap$i"} = $state->{"snap$i"}; @@ -244,8 +290,10 @@ sub param_to_job { sub read_state { if (!-e $STATE) { + make_path $CONFIG_PATH; my $new_fh = IO::File->new("> $STATE"); die "Could not create $STATE: $!\n" if !$new_fh; + print $new_fh "{}"; close($new_fh); return undef; } @@ -287,6 +335,7 @@ sub update_state { if ($job->{state} ne "del") { $state->{state} = $job->{state}; $state->{lsync} = $job->{lsync}; + $state->{vm_type} = $job->{vm_type}; for (my $i = 0; $job->{"snap$i"} ; $i++) { $state->{"snap$i"} = $job->{"snap$i"}; @@ -346,7 +395,7 @@ sub update_cron { } if (!$updated) { - $text .= format_job($job); + $text .= format_job($job); } my $new_fh = IO::File->new("> ${CRONJOBS}.new"); die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh; @@ -374,6 +423,7 @@ sub format_job { $text .= " root"; $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}"; $text .= " --name $job->{name} --maxsnap $job->{maxsnap}"; + $text .= " --limit $job->{limit}" if $job->{limit}; $text .= " --method $job->{method}"; $text .= " --verbose" if $job->{verbose}; $text .= "\n"; @@ -385,16 +435,17 @@ sub list { my $cfg = read_cron(); - my $list = sprintf("%-25s%-15s%-7s%-20s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE"); + my $list = sprintf("%-25s%-25s%-10s%-20s%-6s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON"); my $states = read_state(); foreach my $source (sort keys%{$cfg}) { foreach my $name (sort keys%{$cfg->{$source}}) { $list .= sprintf("%-25s", cut_target_width($source, 25)); - $list .= sprintf("%-15s", cut_target_width($name, 15)); - $list .= sprintf("%-7s", $states->{$source}->{$name}->{state}); - $list .= sprintf("%-20s",$states->{$source}->{$name}->{lsync}); - $list .= sprintf("%-5s\n",$cfg->{$source}->{$name}->{method}); + $list .= sprintf("%-25s", cut_target_width($name, 25)); + $list .= sprintf("%-10s", $states->{$source}->{$name}->{state}); + $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync}); + $list .= sprintf("%-6s", defined($states->{$source}->{$name}->{vm_type}) ? $states->{$source}->{$name}->{vm_type} : "undef"); + $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method}); } } @@ -404,13 +455,20 @@ sub list { sub vm_exists { my ($target) = @_; - my $cmd = ""; - $cmd = "ssh root\@$target->{ip} " if ($target->{ip}); - $cmd .= "qm status $target->{vmid}"; + my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip}; - my $res = run_cmd($cmd); + my $res = undef; + + return undef if !defined($target->{vmid}); + + eval { $res = run_cmd([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) }; + + return "qemu" if $res; + + eval { $res = run_cmd([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) }; + + return "lxc" if $res; - return 1 if ($res =~ m/^status.*$/); return undef; } @@ -428,23 +486,30 @@ sub init { my $dest = parse_target($param->{dest}); if (my $ip = $dest->{ip}) { - run_cmd("ssh-copy-id -i /root/.ssh/id_rsa.pub root\@$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"); + run_cmd(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]); } - die "Pool $dest->{all} does not exists\n" if check_pool_exsits($dest); + die "Pool $dest->{all} does not exists\n" if !check_pool_exists($dest); - my $check = check_pool_exsits($source->{path}, $source->{ip}) if !$source->{vmid} && $source->{path}; + if (!defined($source->{vmid})) { + die "Pool $source->{all} does not exists\n" if !check_pool_exists($source); + } - die "Pool $source->{path} does not exists\n" if undef($check); + my $vm_type = vm_exists($source); + $job->{vm_type} = $vm_type; + $source->{vm_type} = $vm_type; - die "VM $source->{vmid} doesn't exist\n" if $param->{vmid} && !vm_exists($source); + die "VM $source->{vmid} doesn't exist\n" if $source->{vmid} && !$vm_type; die "Config already exists\n" if $cfg->{$job->{source}}->{$job->{name}}; + #check if vm has zfs disks if not die; + get_disks($source, 1) if $source->{vmid}; + update_cron($job); update_state($job); @@ -496,9 +561,8 @@ sub sync { $job = get_job($param); }; - if ($job && $job->{state} ne "ok") { - print "To reset error state use $PROGNAME enable\n" if $job->{state} eq "error" ; - die "Sync will not done! Status: $job->{state}\n"; + if ($job && $job->{state} eq "syncing") { + die "Job --source $param->{source} --name $param->{name} is syncing at the moment"; } my $dest = parse_target($param->{dest}); @@ -507,48 +571,55 @@ sub sync { my $sync_path = sub { my ($source, $dest, $job, $param, $date) = @_; - ($source->{old_snap},$source->{last_snap}) = snapshot_get($source, $dest, $param->{maxsnap}, $param->{name}); + ($source->{old_snap}, $source->{last_snap}) = snapshot_get($source, $dest, $param->{maxsnap}, $param->{name}); - eval{ - snapshot_add($source, $dest, $param->{name}, $date); + snapshot_add($source, $dest, $param->{name}, $date); - send_image($source, $dest, $param); + send_image($source, $dest, $param); + + snapshot_destroy($source, $dest, $param->{method}, $source->{old_snap}) if ($source->{destroy} && $source->{old_snap}); - snapshot_destroy($source, $dest, $param->{method}, $source->{old_snap}) if ($source->{destroy} && $source->{old_snap}); - }; - if(my $err = $@) { - if ($job) { - $job->{state} = "error"; - update_state($job); - unlock($lock_fh); - close($lock_fh); - } - die "$err\n"; - } }; + my $vm_type = vm_exists($source); + $source->{vm_type} = $vm_type; + if ($job) { - die "Job --source $param->{source} --name $param->{name} is syncing" if $job->{state} eq "syncing"; $job->{state} = "syncing"; + $job->{vm_type} = $vm_type if !$job->{vm_type}; update_state($job); } - if ($source->{vmid}) { - die "VM $source->{vmid} doesn't exist\n" if !vm_exists($source); - my $disks = get_disks($source); - - foreach my $disk (sort keys %{$disks}) { - $source->{all} = $disks->{$disk}->{all}; - $source->{pool} = $disks->{$disk}->{pool}; - $source->{path} = $disks->{$disk}->{path} if $disks->{$disk}->{path}; - $source->{last_part} = $disks->{$disk}->{last_part}; + eval{ + if ($source->{vmid}) { + die "VM $source->{vmid} doesn't exist\n" if !$vm_type; + my $disks = get_disks($source); + + foreach my $disk (sort keys %{$disks}) { + $source->{all} = $disks->{$disk}->{all}; + $source->{pool} = $disks->{$disk}->{pool}; + $source->{path} = $disks->{$disk}->{path} if $disks->{$disk}->{path}; + $source->{last_part} = $disks->{$disk}->{last_part}; + &$sync_path($source, $dest, $job, $param, $date); + } + if ($param->{method} eq "ssh" && ($source->{ip} || $dest->{ip})) { + send_config($source, $dest,'ssh'); + } else { + send_config($source, $dest,'local'); + } + } else { &$sync_path($source, $dest, $job, $param, $date); } - if ($param->{method} eq "ssh") { - send_config($source, $dest,'ssh'); + }; + if(my $err = $@) { + if ($job) { + $job->{state} = "error"; + update_state($job); + unlock($lock_fh); + close($lock_fh); + print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n"; } - } else { - &$sync_path($source, $dest, $job, $param, $date); + die "$err\n"; } if ($job) { @@ -564,10 +635,10 @@ sub sync { sub snapshot_get{ my ($source, $dest, $max_snap, $name) = @_; - my $cmd = "zfs list -r -t snapshot -Ho name, -S creation "; - - $cmd .= $source->{all}; - $cmd = "ssh root\@$source->{ip} ".$cmd if $source->{ip}; + my $cmd = []; + push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip}; + push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation'; + push @$cmd, $source->{all}; my $raw = run_cmd($cmd); my $index = 0; @@ -577,7 +648,8 @@ sub snapshot_get{ while ($raw && $raw =~ s/^(.*?)(\n|$)//) { $line = $1; - if ($line =~ m/(rep_$name.*)$/) { + if ($line =~ m/(rep_\Q${name}\E_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})$/) { + $last_snap = $1 if (!$last_snap); $old_snap = $1; $index++; @@ -602,9 +674,9 @@ sub snapshot_add { my $path = "$source->{all}\@$snap_name"; - my $cmd = "zfs snapshot $path"; - $cmd = "ssh root\@$source->{ip} $cmd" if $source->{ip}; - + my $cmd = []; + push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip}; + push @$cmd, 'zfs', 'snapshot', $path; eval{ run_cmd($cmd); }; @@ -627,24 +699,24 @@ sub write_cron { foreach my $source (sort keys%{$cfg}) { foreach my $sync_name (sort keys%{$cfg->{$source}}) { next if $cfg->{$source}->{$sync_name}->{status} ne 'ok'; - $text .= "$PROG_PATH sync"; - $text .= " -source "; - if ($cfg->{$source}->{$sync_name}->{vmid}) { - $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip}; - $text .= "$cfg->{$source}->{$sync_name}->{vmid} "; - } else { - $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip}; - $text .= "$cfg->{$source}->{$sync_name}->{source_pool}"; - $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path}; - } - $text .= " -dest "; - $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip}; - $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}"; - $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path}; - $text .= " -name $sync_name "; - $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit}; - $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap}; - $text .= "\n"; + $text .= "$PROG_PATH sync"; + $text .= " -source "; + if ($cfg->{$source}->{$sync_name}->{vmid}) { + $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip}; + $text .= "$cfg->{$source}->{$sync_name}->{vmid} "; + } else { + $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip}; + $text .= "$cfg->{$source}->{$sync_name}->{source_pool}"; + $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path}; + } + $text .= " -dest "; + $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip}; + $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}"; + $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path}; + $text .= " -name $sync_name "; + $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit}; + $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap}; + $text .= "\n"; } } die "Can't write to cron\n" if (!print($fh $text)); @@ -652,15 +724,22 @@ sub write_cron { } sub get_disks { - my ($target) = @_; + my ($target, $get_err) = @_; - my $cmd = ""; - $cmd = "ssh root\@$target->{ip} " if $target->{ip}; - $cmd .= "qm config $target->{vmid}"; + my $cmd = []; + push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip}; + + if ($target->{vm_type} eq 'qemu') { + push @$cmd, 'qm', 'config', $target->{vmid}; + } elsif ($target->{vm_type} eq 'lxc') { + push @$cmd, 'pct', 'config', $target->{vmid}; + } else { + die "VM Type unknown\n"; + } my $res = run_cmd($cmd); - my $disks = parse_disks($res, $target->{ip}); + my $disks = parse_disks($res, $target->{ip}, $target->{vm_type}, $get_err); return $disks; } @@ -669,9 +748,12 @@ sub run_cmd { my ($cmd) = @_; print "Start CMD\n" if $DEBUG; print Dumper $cmd if $DEBUG; + if (ref($cmd) eq 'ARRAY') { + $cmd = join(' ', map { ref($_) ? $$_ : shell_quote($_) } @$cmd); + } my $output = `$cmd 2>&1`; - die $output if 0 != $?; + die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?; chomp($output); print Dumper $output if $DEBUG; @@ -680,84 +762,109 @@ sub run_cmd { } sub parse_disks { - my ($text, $ip) = @_; + my ($text, $ip, $vm_type, $get_err) = @_; my $disks; my $num = 0; while ($text && $text =~ s/^(.*?)(\n|$)//) { my $line = $1; + my $error = $vm_type eq 'qemu' ? 1 : 0 ; + + next if $line =~ /cdrom|none/; + next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /; + + #QEMU if backup is not set include in sync + next if $vm_type eq 'qemu' && ($line =~ m/backup=(?i:0|no|off|false)/); + + #LXC if backup is not set do no in sync + next if $vm_type eq 'lxc' && ($line =~ m/^mp\d:/) && ($line !~ m/backup=(?i:1|yes|on|true)/); + my $disk = undef; my $stor = undef; - my $is_disk = $line =~ m/^(virtio|ide|scsi|sata){1}\d+: /; - 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($line =~ m/^(?:(?:(?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.*)$/) { + my @parameter = split(/,/,$1); + + foreach my $opt (@parameter) { + if ($opt =~ m/^(?:file=|volume=)?([^:]+:)([A-Za-z0-9\-]+)$/){ + $disk = $2; + $stor = $1; + last; + } + } - die "disk is not on ZFS Storage\n" if $is_disk && !$disk && $line !~ m/cdrom/; + } else { + print "Disk: \"$line\" will not include in pve-sync\n" if $get_err || $error; + next; + } - if($disk && $line !~ m/none/ && $line !~ m/cdrom/ ) { - my $cmd = ""; - $cmd .= "ssh root\@$ip " if $ip; - $cmd .= "pvesm path $stor$disk"; - my $path = run_cmd($cmd); + my $cmd = []; + push @$cmd, 'ssh', "root\@$ip", '--' if $ip; + push @$cmd, 'pvesm', 'path', "$stor$disk"; + my $path = run_cmd($cmd); - if ($path =~ m/^\/dev\/zvol\/(\w+).*(\/$disk)$/) { + die "Get no path from pvesm path $stor$disk\n" if !$path; + + if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) { + + my @array = split('/', $1); + $disks->{$num}->{pool} = shift(@array); + $disks->{$num}->{all} = $disks->{$num}->{pool}; + if (0 < @array) { + $disks->{$num}->{path} = join('/', @array); + $disks->{$num}->{all} .= "\/$disks->{$num}->{path}"; + } + $disks->{$num}->{last_part} = $disk; + $disks->{$num}->{all} .= "\/$disk"; - my @array = split('/', $1); - $disks->{$num}->{pool} = pop(@array); - $disks->{$num}->{all} = $disks->{$num}->{pool}; - if (0 < @array) { - $disks->{$num}->{path} = join('/', @array); - $disks->{$num}->{all} .= "\/$disks->{$num}->{path}"; - } - $disks->{$num}->{last_part} = $disk; - $disks->{$num}->{all} .= "\/$disk"; + $num++; + } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w+.+)(\/(\w+.*))*(\/$disk)$/) { - $num++; + $disks->{$num}->{pool} = $1; + $disks->{$num}->{all} = $disks->{$num}->{pool}; - } else { - die "ERROR: in path\n"; + if ($2) { + $disks->{$num}->{path} = $3; + $disks->{$num}->{all} .= "\/$disks->{$num}->{path}"; } + + $disks->{$num}->{last_part} = $disk; + $disks->{$num}->{all} .= "\/$disk"; + + $num++; + + } else { + die "ERROR: in path\n"; } } + die "Vm include no disk on zfs.\n" if !$disks->{0}; return $disks; } sub snapshot_destroy { my ($source, $dest, $method, $snap) = @_; - my $zfscmd = "zfs destroy "; + my @zfscmd = ('zfs', 'destroy'); my $snapshot = "$source->{all}\@$snap"; eval { if($source->{ip} && $method eq 'ssh'){ - run_cmd("ssh root\@$source->{ip} $zfscmd $snapshot"); + run_cmd(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]); } else { - run_cmd("$zfscmd $snapshot"); + run_cmd([@zfscmd, $snapshot]); } }; if (my $erro = $@) { warn "WARN: $erro"; } if ($dest) { - my $ssh = $dest->{ip} ? "ssh root\@$dest->{ip}" : ""; + my @ssh = $dest->{ip} ? ('ssh', "root\@$dest->{ip}", '--') : (); my $path = "$dest->{all}\/$source->{last_part}"; eval { - run_cmd("$ssh $zfscmd $path\@$snap "); + run_cmd([@ssh, @zfscmd, "$path\@$snap"]); }; if (my $erro = $@) { warn "WARN: $erro"; @@ -766,22 +873,22 @@ sub snapshot_destroy { } sub snapshot_exist { - my ($source ,$dest, $method) = @_; + my ($source , $dest, $method) = @_; - my $cmd = ""; - $cmd = "ssh root\@$dest->{ip} " if $dest->{ip}; - $cmd .= "zfs list -rt snapshot -Ho name $dest->{all}"; - $cmd .= "\/$source->{last_part}\@$source->{old_snap}"; + my $cmd = []; + push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip}; + push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name'; + push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}"; my $text = ""; eval {$text =run_cmd($cmd);}; - if (my $erro = $@) { + if (my $erro =$@) { warn "WARN: $erro"; return undef; } while ($text && $text =~ s/^(.*?)(\n|$)//) { - my $line = $1; + my $line =$1; return 1 if $line =~ m/^.*$source->{old_snap}$/; } } @@ -789,127 +896,136 @@ sub snapshot_exist { sub send_image { my ($source, $dest, $param) = @_; - my $cmd = ""; + my $cmd = []; - $cmd .= "ssh root\@$source->{ip} " if $source->{ip}; - $cmd .= "zfs send "; - $cmd .= "-v " if $param->{verbose}; + push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\@$source->{ip}", '--' if $source->{ip}; + push @$cmd, 'zfs', 'send'; + push @$cmd, '-v' if $param->{verbose}; - if($source->{last_snap} && snapshot_exist($source ,$dest, $param->{method})) { - $cmd .= "-i $source->{all}\@$source->{old_snap} $source->{all}\@$source->{new_snap} "; - } else { - $cmd .= "$source->{all}\@$source->{new_snap} "; + if($source->{last_snap} && snapshot_exist($source , $dest, $param->{method})) { + push @$cmd, '-i', "$source->{all}\@$source->{last_snap}"; } + push @$cmd, '--', "$source->{all}\@$source->{new_snap}"; if ($param->{limit}){ my $bwl = $param->{limit}*1024; - $cmd .= "| cstream -t $bwl"; + push @$cmd, \'|', 'cstream', '-t', $bwl; } - $cmd .= "| "; - $cmd .= "ssh root\@$dest->{ip} " if $dest->{ip}; - $cmd .= "zfs recv $dest->{all}"; - $cmd .= "\/$source->{last_part}\@$source->{new_snap}"; + my $target = "$dest->{all}/$source->{last_part}"; + $target =~ s!/+!/!g; - eval { - run_cmd($cmd) - }; + push @$cmd, \'|'; + push @$cmd, 'ssh', '-o', 'BatchMode=yes', "root\@$dest->{ip}", '--' if $dest->{ip}; + push @$cmd, 'zfs', 'recv', '-F', '--'; + push @$cmd, "$target"; - if (my $erro = $@) { - snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}); - die $erro; - }; -} + eval { + run_cmd($cmd) + }; + + if (my $erro = $@) { + snapshot_destroy($source, undef, $param->{method}, $source->{new_snap}); + die $erro; + }; + } -sub send_config{ - my ($source, $dest, $method) = @_; + sub send_config{ + my ($source, $dest, $method) = @_; - my $source_target ="$QEMU_CONF$source->{vmid}.conf"; - my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$source->{new_snap}"; + my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF/$source->{vmid}.conf": "$LXC_CONF/$source->{vmid}.conf"; + my $dest_target_new ="$source->{vmid}.conf.$source->{vm_type}.$source->{new_snap}"; - if ($method eq 'ssh'){ - if ($dest->{ip} && $source->{ip}) { - run_cmd("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p"); - run_cmd("scp root\@$source->{ip}:$source_target root\@$dest->{ip}:$dest_target_new"); - } elsif ($dest->{ip}) { - run_cmd("ssh root\@$dest->{ip} mkdir $CONFIG_PATH -p"); - run_cmd("scp $source_target root\@$dest->{ip}:$dest_target_new"); - } elsif ($source->{ip}) { - run_cmd("mkdir $CONFIG_PATH -p"); - run_cmd("scp root\@$source->{ip}:$source_target $dest_target_new"); - } + my $config_dir = $dest->{last_part} ? "${CONFIG_PATH}/$dest->{last_part}" : $CONFIG_PATH; - if ($source->{destroy}){ - my $dest_target_old ="$CONFIG_PATH$source->{vmid}.conf.$source->{old_snap}"; - if($dest->{ip}){ - run_cmd("ssh root\@$dest->{ip} rm -f $dest_target_old"); - } else { - run_cmd("rm -f $dest_target_old"); + $dest_target_new = $config_dir.'/'.$dest_target_new; + + if ($method eq 'ssh'){ + if ($dest->{ip} && $source->{ip}) { + run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]); + run_cmd(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]); + } elsif ($dest->{ip}) { + run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]); + run_cmd(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]); + } elsif ($source->{ip}) { + run_cmd(['mkdir', '-p', '--', $config_dir]); + run_cmd(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]); + } + + if ($source->{destroy}){ + my $dest_target_old ="${config_dir}/$source->{vmid}.conf.$source->{vm_type}.$source->{old_snap}"; + if($dest->{ip}){ + run_cmd(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]); + } else { + run_cmd(['rm', '-f', '--', $dest_target_old]); + } } + } elsif ($method eq 'local') { + run_cmd(['mkdir', '-p', '--', $config_dir]); + run_cmd(['cp', $source_target, $dest_target_new]); } } -} -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); + 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; -} + return $datestamp; + } -sub status { - my $cfg = read_cron(); + sub status { + my $cfg = read_cron(); - my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS"); + my $status_list = sprintf("%-25s%-25s%-10s\n", "SOURCE", "NAME", "STATUS"); - my $states = read_state(); + my $states = read_state(); - foreach my $source (sort keys%{$cfg}) { - foreach my $sync_name (sort keys%{$cfg->{$source}}) { - $status_list .= sprintf("%-25s", cut_target_width($source, 25)); - $status_list .= sprintf("%-15s", cut_target_width($sync_name, 25)); - $status_list .= "$states->{$source}->{$sync_name}->{state}\n"; + foreach my $source (sort keys%{$cfg}) { + foreach my $sync_name (sort keys%{$cfg->{$source}}) { + $status_list .= sprintf("%-25s", cut_target_width($source, 25)); + $status_list .= sprintf("%-25s", cut_target_width($sync_name, 25)); + $status_list .= "$states->{$source}->{$sync_name}->{state}\n"; + } } - } - return $status_list; -} + return $status_list; + } -sub enable_job { - my ($param) = @_; + sub enable_job { + my ($param) = @_; - my $job = get_job($param); - $job->{state} = "ok"; - update_state($job); - update_cron($job); -} + my $job = get_job($param); + $job->{state} = "ok"; + update_state($job); + update_cron($job); + } -sub disable_job { - my ($param) = @_; + sub disable_job { + my ($param) = @_; - my $job = get_job($param); - $job->{state} = "stopped"; - update_state($job); - update_cron($job); -} + my $job = get_job($param); + $job->{state} = "stopped"; + update_state($job); + update_cron($job); + } -my $command = $ARGV[0]; + my $command = $ARGV[0]; -my $commands = {'destroy' => 1, - 'create' => 1, - 'sync' => 1, - 'list' => 1, - 'status' => 1, - 'help' => 1, - 'enable' => 1, - 'disable' => 1}; + my $commands = {'destroy' => 1, + 'create' => 1, + 'sync' => 1, + 'list' => 1, + 'status' => 1, + 'help' => 1, + 'enable' => 1, + 'disable' => 1}; -if (!$command || !$commands->{$command}) { - usage(); - die "\n"; -} + if (!$command || !$commands->{$command}) { + usage(); + die "\n"; + } -my $help_sync = "$PROGNAME sync -dest -source [OPTIONS]\n + my $help_sync = "$PROGNAME sync -dest -source [OPTIONS]\n \twill sync one time\n \t-dest\tstring\n \t\tthe destination target is like [IP:][/Path]\n @@ -923,9 +1039,9 @@ my $help_sync = "$PROGNAME sync -dest -source [OPTIONS]\n \t-source\tstring\n \t\tthe source can be an or [IP:][/Path]\n"; -my $help_create = "$PROGNAME create -dest -source [OPTIONS]/n + my $help_create = "$PROGNAME create -dest -source [OPTIONS]/n \tCreate a sync Job\n -\t-dest\tstringn\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 @@ -938,165 +1054,148 @@ my $help_create = "$PROGNAME create -dest -source [OPTIONS]/n \t-source\tstring\n \t\tthe source can be an or [IP:][/Path]\n"; -my $help_destroy = "$PROGNAME destroy -source [OPTIONS]\n + my $help_destroy = "$PROGNAME 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 = "$PROGNAME help [OPTIONS]\n + my $help_help = "$PROGNAME 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 = "$PROGNAME list\n + my $help_list = "$PROGNAME list\n \tGet a List of all scheduled Sync Jobs\n"; -my $help_status = "$PROGNAME status\n + my $help_status = "$PROGNAME status\n \tGet the status of all scheduled Sync Jobs\n"; -my $help_enable = "$PROGNAME enable -source [OPTIONS]\n + my $help_enable = "$PROGNAME enable -source [OPTIONS]\n \tenable a syncjob and reset error\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_disable = "$PROGNAME disable -source [OPTIONS]\n + my $help_disable = "$PROGNAME disable -source [OPTIONS]\n \tpause a syncjob\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"; -sub help { - my ($command) = @_; + sub help { + my ($command) = @_; - switch($command){ - case 'help' - { + if ($command eq 'help') { die "$help_help\n"; - } - case 'sync' - { + + } elsif ($command eq 'sync') { die "$help_sync\n"; - } - case 'destroy' - { + + } elsif ($command eq 'destroy') { die "$help_destroy\n"; - } - case 'create' - { + + } elsif ($command eq 'create') { die "$help_create\n"; - } - case 'list' - { + + } elsif ($command eq 'list') { die "$help_list\n"; - } - case 'status' - { + + } elsif ($command eq 'status') { die "$help_status\n"; - } - case 'enable' - { - die "$help_enable\n"; - } - case 'disable' - { + + } elsif ($command eq 'enable') { die "$help_enable\n"; - } - } -} + } elsif ($command eq 'disable') { + die "$help_disable\n"; -my @arg = @ARGV; -my $param = parse_argv(@arg); + } + + } + my @arg = @ARGV; + my $param = parse_argv(@arg); -switch($command) { - case "destroy" - { + if ($command eq 'destroy') { die "$help_destroy\n" if !$param->{source}; + check_target($param->{source}); destroy_job($param); - } - case "sync" - { + + } elsif ($command eq 'sync') { die "$help_sync\n" if !$param->{source} || !$param->{dest}; + check_target($param->{source}); check_target($param->{dest}); sync($param); - } - case "create" - { + + } elsif ($command eq 'create') { die "$help_create\n" if !$param->{source} || !$param->{dest}; + check_target($param->{source}); check_target($param->{dest}); init($param); - } - case "status" - { + + } elsif ($command eq 'status') { print status(); - } - case "list" - { + + } elsif ($command eq 'list') { print list(); - } - case "help" - { + + } elsif ($command eq 'help') { my $help_command = $ARGV[1]; + if ($help_command && $commands->{$help_command}) { print help($help_command); + } if ($param->{verbose} == 1){ exec("man $PROGNAME"); + } else { usage(1); + } - } - case "enable" - { + + } elsif ($command eq 'enable') { die "$help_enable\n" if !$param->{source}; + check_target($param->{source}); enable_job($param); - } - case "disable" - { + + } elsif ($command eq 'disable') { die "$help_disable\n" if !$param->{source}; + check_target($param->{source}); disable_job($param); - } -} -sub usage { - my ($help) = @_; - - print("ERROR:\tno command specified\n") if !$help; - print("USAGE:\t$PROGNAME [ARGS] [OPTIONS]\n"); - print("\t$PROGNAME help [] [OPTIONS]\n\n"); - print("\t$PROGNAME create -dest -source [OPTIONS]\n"); - print("\t$PROGNAME destroy -source [OPTIONS]\n"); - print("\t$PROGNAME disable -source [OPTIONS]\n"); - print("\t$PROGNAME enable -source [OPTIONS]\n"); - print("\t$PROGNAME list\n"); - print("\t$PROGNAME status\n"); - print("\t$PROGNAME sync -dest -source [OPTIONS]\n"); -} - -sub check_target { - my ($target) = @_; + } - chomp($target); + sub usage { + my ($help) = @_; + + print("ERROR:\tno command specified\n") if !$help; + print("USAGE:\t$PROGNAME [ARGS] [OPTIONS]\n"); + print("\t$PROGNAME help [] [OPTIONS]\n\n"); + print("\t$PROGNAME create -dest -source [OPTIONS]\n"); + print("\t$PROGNAME destroy -source [OPTIONS]\n"); + print("\t$PROGNAME disable -source [OPTIONS]\n"); + print("\t$PROGNAME enable -source [OPTIONS]\n"); + print("\t$PROGNAME list\n"); + print("\t$PROGNAME status\n"); + print("\t$PROGNAME sync -dest -source [OPTIONS]\n"); + } - if($target !~ m/(\d+.\d+.\d+.\d+:)?([\w\-\_\/]+)(\/.+)?/) { - print("ERROR:\t$target is not valid.\n\tUse [IP:][/Path]!\n"); - return 1; + sub check_target { + my ($target) = @_; + parse_target($target); } - return undef; -} __END__ @@ -1233,12 +1332,10 @@ add sync job from local VM to remote ZFS Server pve-zsync create -source=100 -dest=192.168.1.2:zfspool =head1 IMPORTANT FILES - -Cron jobs are stored at /etc/cron.d/pve-zsync -The VM config get copied on the destination machine to /var/pve-zsync/ +Cron jobs and config are stored at /etc/cron.d/pve-zsync -The config is stored at /var/pve-zsync/ +The VM config get copied on the destination machine to /var/lib/pve-zsync/ =head1 COPYRIGHT AND DISCLAIMER