]>
git.proxmox.com Git - pve-zsync.git/blob - pve-zsync
c54441d8577fe5e2bd17da6ecc605ba773e33052
5 use Data
::Dumper
qw(Dumper);
6 use Fcntl
qw(:flock SEEK_END);
7 use Getopt
::Long
qw(GetOptionsFromArray);
8 use File
::Copy
qw(move);
9 use File
::Path
qw(make_path);
13 use String
::ShellQuote
'shell_quote';
15 my $PROGNAME = "pve-zsync";
16 my $CONFIG_PATH = "/var/lib/${PROGNAME}";
17 my $STATE = "${CONFIG_PATH}/sync_state";
18 my $CRONJOBS = "/etc/cron.d/$PROGNAME";
19 my $PATH = "/usr/sbin";
20 my $PVE_DIR = "/etc/pve/local";
21 my $QEMU_CONF = "${PVE_DIR}/qemu-server";
22 my $LXC_CONF = "${PVE_DIR}/lxc";
23 my $LOCKFILE = "$CONFIG_PATH/${PROGNAME}.lock";
24 my $PROG_PATH = "$PATH/${PROGNAME}";
28 my $IPV4OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
29 my $IPV4RE = "(?:(?:$IPV4OCTET\\.){3}$IPV4OCTET)";
30 my $IPV6H16 = "(?:[0-9a-fA-F]{1,4})";
31 my $IPV6LS32 = "(?:(?:$IPV4RE|$IPV6H16:$IPV6H16))";
34 "(?:(?:" . "(?:$IPV6H16:){6})$IPV6LS32)|" .
35 "(?:(?:" . "::(?:$IPV6H16:){5})$IPV6LS32)|" .
36 "(?:(?:(?:" . "$IPV6H16)?::(?:$IPV6H16:){4})$IPV6LS32)|" .
37 "(?:(?:(?:(?:$IPV6H16:){0,1}$IPV6H16)?::(?:$IPV6H16:){3})$IPV6LS32)|" .
38 "(?:(?:(?:(?:$IPV6H16:){0,2}$IPV6H16)?::(?:$IPV6H16:){2})$IPV6LS32)|" .
39 "(?:(?:(?:(?:$IPV6H16:){0,3}$IPV6H16)?::(?:$IPV6H16:){1})$IPV6LS32)|" .
40 "(?:(?:(?:(?:$IPV6H16:){0,4}$IPV6H16)?::" . ")$IPV6LS32)|" .
41 "(?:(?:(?:(?:$IPV6H16:){0,5}$IPV6H16)?::" . ")$IPV6H16)|" .
42 "(?:(?:(?:(?:$IPV6H16:){0,6}$IPV6H16)?::" . ")))";
44 my $HOSTv4RE0 = "(?:[\\w\\.\\-_]+|$IPV4RE)"; # hostname or ipv4 address
45 my $HOSTv4RE1 = "(?:$HOSTv4RE0|\\[$HOSTv4RE0\\])"; # these may be in brackets, too
46 my $HOSTRE = "(?:$HOSTv4RE1|\\[$IPV6RE\\])"; # ipv6 must always be in brackets
47 # targets are either a VMID, or a 'host:zpool/path' with 'host:' being optional
48 my $TARGETRE = qr!^(?:($HOSTRE):)?(\d+|(?:[\w\-_]+)(/.+)?)$!;
50 check_bin
('cstream');
58 foreach my $p (split (/:/, $ENV{PATH
})) {
65 die "unable to find command '$bin'\n";
68 sub cut_target_width
{
69 my ($target, $max) = @_;
71 return $target if (length($target) <= $max);
72 my @spl = split('/', $target);
74 my $count = length($spl[@spl-1]);
75 return "..\/".substr($spl[@spl-1],($count-$max)+3 , $count) if $count > $max;
77 $count += length($spl[0]) if @spl > 1;
78 return substr($spl[0], 0, $max-4-length
($spl[@spl-1]))."\/..\/".$spl[@spl-1] if $count > $max;
81 $rest = $max-$count if ($max-$count > 0);
83 return "$spl[0]".substr($target, length($spl[0]), $rest)."..\/".$spl[@spl-1];
88 flock($fh, LOCK_EX
) || die "Can't lock config - $!\n";
93 flock($fh, LOCK_UN
) || die "Can't unlock config- $!\n";
97 my ($source, $name, $status) = @_;
99 if ($status->{$source->{all
}}->{$name}->{status
}) {
106 sub check_pool_exists
{
110 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
111 push @$cmd, 'zfs', 'list', '-H', '--', $target->{all
};
125 my $errstr = "$text : is not a valid input! Use [IP:]<VMID> or [IP:]<ZFSPool>[/Path]";
128 if ($text !~ $TARGETRE) {
132 $target->{ip
} = $1 if $1;
133 my @parts = split('/', $2);
135 $target->{ip
} =~ s/^\[(.*)\]$/$1/ if $target->{ip
};
137 my $pool = $target->{pool
} = shift(@parts);
138 die "$errstr\n" if !$pool;
140 if ($pool =~ m/^\d+$/) {
141 $target->{vmid
} = $pool;
142 delete $target->{pool
};
145 return $target if (@parts == 0);
146 $target->{last_part
} = pop(@parts);
152 $target->{path
} = join('/', @parts);
160 #This is for the first use to init file;
162 my $new_fh = IO
::File-
>new("> $CRONJOBS");
163 die "Could not create $CRONJOBS: $!\n" if !$new_fh;
168 my $fh = IO
::File-
>new("< $CRONJOBS");
169 die "Could not open file $CRONJOBS: $!\n" if !$fh;
175 return encode_cron
(@text);
182 $param->{dest
} = undef;
183 $param->{source
} = undef;
184 $param->{verbose
} = undef;
185 $param->{limit
} = undef;
186 $param->{maxsnap
} = undef;
187 $param->{name
} = undef;
188 $param->{skip
} = undef;
189 $param->{method} = undef;
191 my ($ret, $ar) = GetOptionsFromArray
(\
@arg,
192 'dest=s' => \
$param->{dest
},
193 'source=s' => \
$param->{source
},
194 'verbose' => \
$param->{verbose
},
195 'limit=i' => \
$param->{limit
},
196 'maxsnap=i' => \
$param->{maxsnap
},
197 'name=s' => \
$param->{name
},
198 'skip' => \
$param->{skip
},
199 'method=s' => \
$param->{method});
202 die "can't parse options\n";
205 $param->{name
} = "default" if !$param->{name
};
206 $param->{maxsnap
} = 1 if !$param->{maxsnap
};
207 $param->{method} = "ssh" if !$param->{method};
212 sub add_state_to_job
{
215 my $states = read_state
();
216 my $state = $states->{$job->{source
}}->{$job->{name
}};
218 $job->{state} = $state->{state};
219 $job->{lsync
} = $state->{lsync
};
220 $job->{vm_type
} = $state->{vm_type
};
222 for (my $i = 0; $state->{"snap$i"}; $i++) {
223 $job->{"snap$i"} = $state->{"snap$i"};
234 while (my $line = shift(@text)) {
236 my @arg = split('\s', $line);
237 my $param = parse_argv
(@arg);
239 if ($param->{source
} && $param->{dest
}) {
240 $cfg->{$param->{source
}}->{$param->{name
}}->{dest
} = $param->{dest
};
241 $cfg->{$param->{source
}}->{$param->{name
}}->{verbose
} = $param->{verbose
};
242 $cfg->{$param->{source
}}->{$param->{name
}}->{limit
} = $param->{limit
};
243 $cfg->{$param->{source
}}->{$param->{name
}}->{maxsnap
} = $param->{maxsnap
};
244 $cfg->{$param->{source
}}->{$param->{name
}}->{skip
} = $param->{skip
};
245 $cfg->{$param->{source
}}->{$param->{name
}}->{method} = $param->{method};
257 my $source = parse_target
($param->{source
});
258 my $dest = parse_target
($param->{dest
}) if $param->{dest
};
260 $job->{name
} = !$param->{name
} ?
"default" : $param->{name
};
261 $job->{dest
} = $param->{dest
} if $param->{dest
};
262 $job->{method} = "local" if !$dest->{ip
} && !$source->{ip
};
263 $job->{method} = "ssh" if !$job->{method};
264 $job->{limit
} = $param->{limit
};
265 $job->{maxsnap
} = $param->{maxsnap
} if $param->{maxsnap
};
266 $job->{source
} = $param->{source
};
274 make_path
$CONFIG_PATH;
275 my $new_fh = IO
::File-
>new("> $STATE");
276 die "Could not create $STATE: $!\n" if !$new_fh;
282 my $fh = IO
::File-
>new("< $STATE");
283 die "Could not open file $STATE: $!\n" if !$fh;
286 my $states = decode_json
($text);
300 $in_fh = IO
::File-
>new("< $STATE");
301 die "Could not open file $STATE: $!\n" if !$in_fh;
306 my $out_fh = IO
::File-
>new("> $STATE.new");
307 die "Could not open file ${STATE}.new: $!\n" if !$out_fh;
312 $states = decode_json
($text);
313 $state = $states->{$job->{source
}}->{$job->{name
}};
316 if ($job->{state} ne "del") {
317 $state->{state} = $job->{state};
318 $state->{lsync
} = $job->{lsync
};
319 $state->{vm_type
} = $job->{vm_type
};
321 for (my $i = 0; $job->{"snap$i"} ; $i++) {
322 $state->{"snap$i"} = $job->{"snap$i"};
324 $states->{$job->{source
}}->{$job->{name
}} = $state;
327 delete $states->{$job->{source
}}->{$job->{name
}};
328 delete $states->{$job->{source
}} if !keys %{$states->{$job->{source
}}};
331 $text = encode_json
($states);
335 move
("$STATE.new", $STATE);
350 my $header = "SHELL=/bin/sh\n";
351 $header .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n\n";
353 my $fh = IO
::File-
>new("< $CRONJOBS");
354 die "Could not open file $CRONJOBS: $!\n" if !$fh;
359 while (my $line = shift(@test)) {
361 if ($line =~ m/source $job->{source} .*name $job->{name} /) {
363 next if $job->{state} eq "del";
364 $text .= format_job
($job, $line);
366 if (($line_no < 3) && ($line =~ /^(PATH|SHELL)/ )) {
375 $text = "$header$text";
379 $text .= format_job
($job);
381 my $new_fh = IO
::File-
>new("> ${CRONJOBS}.new");
382 die "Could not open file ${CRONJOBS}.new: $!\n" if !$new_fh;
384 die "can't write to $CRONJOBS.new\n" if !print($new_fh $text);
387 die "can't move $CRONJOBS.new: $!\n" if !move
("${CRONJOBS}.new", "$CRONJOBS");
392 my ($job, $line) = @_;
395 if ($job->{state} eq "stopped") {
399 $line =~ /^#*(.+) root/;
402 $text .= "*/$INTERVAL * * * *";
405 $text .= " $PROGNAME sync --source $job->{source} --dest $job->{dest}";
406 $text .= " --name $job->{name} --maxsnap $job->{maxsnap}";
407 $text .= " --method $job->{method}";
408 $text .= " --verbose" if $job->{verbose
};
416 my $cfg = read_cron
();
418 my $list = sprintf("%-25s%-10s%-7s%-20s%-5s%-5s\n" , "SOURCE", "NAME", "STATE", "LAST SYNC", "TYPE", "CON");
420 my $states = read_state
();
421 foreach my $source (sort keys%{$cfg}) {
422 foreach my $name (sort keys%{$cfg->{$source}}) {
423 $list .= sprintf("%-25s", cut_target_width
($source, 25));
424 $list .= sprintf("%-10s", cut_target_width
($name, 10));
425 $list .= sprintf("%-7s", $states->{$source}->{$name}->{state});
426 $list .= sprintf("%-20s", $states->{$source}->{$name}->{lsync
});
427 $list .= sprintf("%-5s", $states->{$source}->{$name}->{vm_type
});
428 $list .= sprintf("%-5s\n", $cfg->{$source}->{$name}->{method});
438 my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip
};
442 eval { $res = run_cmd
([@cmd, 'ls', "$QEMU_CONF/$target->{vmid}.conf"]) };
444 return "qemu" if $res;
446 eval { $res = run_cmd
([@cmd, 'ls', "$LXC_CONF/$target->{vmid}.conf"]) };
448 return "lxc" if $res;
456 my $cfg = read_cron
();
458 my $job = param_to_job
($param);
460 $job->{state} = "ok";
463 my $source = parse_target
($param->{source
});
464 my $dest = parse_target
($param->{dest
});
466 if (my $ip = $dest->{ip
}) {
467 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
470 if (my $ip = $source->{ip
}) {
471 run_cmd
(['ssh-copy-id', '-i', '/root/.ssh/id_rsa.pub', "root\@$ip"]);
474 die "Pool $dest->{all} does not exists\n" if check_pool_exists
($dest);
476 my $check = check_pool_exists
($source->{path
}, $source->{ip
}) if !$source->{vmid
} && $source->{path
};
478 die "Pool $source->{path} does not exists\n" if undef($check);
480 my $vm_type = vm_exists
($source);
481 $job->{vm_type
} = $vm_type;
483 die "VM $source->{vmid} doesn't exist\n" if $param->{vmid
} && !$vm_type;
485 die "Config already exists\n" if $cfg->{$job->{source
}}->{$job->{name
}};
491 sync
($param) if !$param->{skip
};
502 my $cfg = read_cron
();
504 if (!$cfg->{$param->{source
}}->{$param->{name
}}) {
505 die "Job with source $param->{source} and name $param->{name} does not exist\n" ;
507 my $job = $cfg->{$param->{source
}}->{$param->{name
}};
508 $job->{name
} = $param->{name
};
509 $job->{source
} = $param->{source
};
510 $job = add_state_to_job
($job);
518 my $job = get_job
($param);
519 $job->{state} = "del";
528 my $lock_fh = IO
::File-
>new("> $LOCKFILE");
529 die "Can't open Lock File: $LOCKFILE $!\n" if !$lock_fh;
532 my $date = get_date
();
535 $job = get_job
($param);
538 if ($job && $job->{state} eq "syncing") {
539 die "Job --source $param->{source} --name $param->{name} is syncing at the moment";
542 my $dest = parse_target
($param->{dest
});
543 my $source = parse_target
($param->{source
});
545 my $sync_path = sub {
546 my ($source, $dest, $job, $param, $date) = @_;
548 ($source->{old_snap
}, $source->{last_snap
}) = snapshot_get
($source, $dest, $param->{maxsnap
}, $param->{name
});
550 snapshot_add
($source, $dest, $param->{name
}, $date);
552 send_image
($source, $dest, $param);
554 snapshot_destroy
($source, $dest, $param->{method}, $source->{old_snap
}) if ($source->{destroy
} && $source->{old_snap
});
558 my $vm_type = vm_exists
($source);
559 $source->{vm_type
} = $vm_type;
562 $job->{state} = "syncing";
563 $job->{vm_type
} = $vm_type if !$job->{vm_type
};
568 if ($source->{vmid
}) {
569 die "VM $source->{vmid} doesn't exist\n" if !$vm_type;
570 my $disks = get_disks
($source);
572 foreach my $disk (sort keys %{$disks}) {
573 $source->{all
} = $disks->{$disk}->{all
};
574 $source->{pool
} = $disks->{$disk}->{pool
};
575 $source->{path
} = $disks->{$disk}->{path
} if $disks->{$disk}->{path
};
576 $source->{last_part
} = $disks->{$disk}->{last_part
};
577 &$sync_path($source, $dest, $job, $param, $date);
579 if ($param->{method} eq "ssh" && ($source->{ip
} || $dest->{ip
})) {
580 send_config
($source, $dest,'ssh');
582 send_config
($source, $dest,'local');
585 &$sync_path($source, $dest, $job, $param, $date);
590 $job->{state} = "error";
594 print "Job --source $param->{source} --name $param->{name} got an ERROR!!!\nERROR Message:\n";
600 $job->{state} = "ok";
601 $job->{lsync
} = $date;
610 my ($source, $dest, $max_snap, $name) = @_;
613 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
614 push @$cmd, 'zfs', 'list', '-r', '-t', 'snapshot', '-Ho', 'name', '-S', 'creation';
615 push @$cmd, $source->{all
};
617 my $raw = run_cmd
($cmd);
620 my $last_snap = undef;
623 while ($raw && $raw =~ s/^(.*?)(\n|$)//) {
625 if ($line =~ m/(rep_$name.*)$/) {
627 $last_snap = $1 if (!$last_snap);
630 if ($index == $max_snap) {
631 $source->{destroy
} = 1;
637 return ($old_snap, $last_snap) if $last_snap;
643 my ($source, $dest, $name, $date) = @_;
645 my $snap_name = "rep_$name\_".$date;
647 $source->{new_snap
} = $snap_name;
649 my $path = "$source->{all}\@$snap_name";
652 push @$cmd, 'ssh', "root\@$source->{ip}", '--', if $source->{ip
};
653 push @$cmd, 'zfs', 'snapshot', $path;
659 snapshot_destroy
($source, $dest, 'ssh', $snap_name);
667 my $text = "SHELL=/bin/sh\n";
668 $text .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n";
670 my $fh = IO
::File-
>new("> $CRONJOBS");
671 die "Could not open file: $!\n" if !$fh;
673 foreach my $source (sort keys%{$cfg}) {
674 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
675 next if $cfg->{$source}->{$sync_name}->{status
} ne 'ok';
676 $text .= "$PROG_PATH sync";
677 $text .= " -source ";
678 if ($cfg->{$source}->{$sync_name}->{vmid
}) {
679 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
680 $text .= "$cfg->{$source}->{$sync_name}->{vmid} ";
682 $text .= "$cfg->{$source}->{$sync_name}->{source_ip}:" if $cfg->{$source}->{$sync_name}->{source_ip
};
683 $text .= "$cfg->{$source}->{$sync_name}->{source_pool}";
684 $text .= "$cfg->{$source}->{$sync_name}->{source_path}" if $cfg->{$source}->{$sync_name}->{source_path
};
687 $text .= "$cfg->{$source}->{$sync_name}->{dest_ip}:" if $cfg->{$source}->{$sync_name}->{dest_ip
};
688 $text .= "$cfg->{$source}->{$sync_name}->{dest_pool}";
689 $text .= "$cfg->{$source}->{$sync_name}->{dest_path}" if $cfg->{$source}->{$sync_name}->{dest_path
};
690 $text .= " -name $sync_name ";
691 $text .= " -limit $cfg->{$source}->{$sync_name}->{limit}" if $cfg->{$source}->{$sync_name}->{limit
};
692 $text .= " -maxsnap $cfg->{$source}->{$sync_name}->{maxsnap}" if $cfg->{$source}->{$sync_name}->{maxsnap
};
696 die "Can't write to cron\n" if (!print($fh $text));
704 push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip
};
706 if ($target->{vm_type
} eq 'qemu') {
707 push @$cmd, 'qm', 'config', $target->{vmid
};
708 } elsif ($target->{vm_type
} eq 'lxc') {
709 push @$cmd, 'pct', 'config', $target->{vmid
};
711 die "VM Type unknown\n";
714 my $res = run_cmd
($cmd);
716 my $disks = parse_disks
($res, $target->{ip
}, $target->{vm_type
});
723 print "Start CMD\n" if $DEBUG;
724 print Dumper
$cmd if $DEBUG;
725 if (ref($cmd) eq 'ARRAY') {
726 $cmd = join(' ', map { ref($_) ?
$$_ : shell_quote
($_) } @$cmd);
728 my $output = `$cmd 2>&1`;
730 die "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?;
733 print Dumper
$output if $DEBUG;
734 print "END CMD\n" if $DEBUG;
739 my ($text, $ip, $vm_type) = @_;
744 while ($text && $text =~ s/^(.*?)(\n|$)//) {
747 next if $line =~ /cdrom|none/;
748 next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /;
752 if($line =~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.+:)([A-Za-z0-9\-]+),(.*)$/) {
756 die "disk is not on ZFS Storage\n";
760 push @$cmd, 'ssh', "root\@$ip", '--' if $ip;
761 push @$cmd, 'pvesm', 'path', "$stor$disk";
762 my $path = run_cmd
($cmd);
764 if ($vm_type eq 'qemu' && $path =~ m/^\/dev\
/zvol\/(\w
+.*)(\
/$disk)$/) {
766 my @array = split('/', $1);
767 $disks->{$num}->{pool
} = shift(@array);
768 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
770 $disks->{$num}->{path
} = join('/', @array);
771 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
773 $disks->{$num}->{last_part
} = $disk;
774 $disks->{$num}->{all
} .= "\/$disk";
777 } elsif ($vm_type eq 'lxc' && $path =~ m/^\/(\w
+.+)(\
/(\w+.*))*(\/$disk)$/) {
779 $disks->{$num}->{pool
} = $1;
780 $disks->{$num}->{all
} = $disks->{$num}->{pool
};
783 $disks->{$num}->{path
} = $3;
784 $disks->{$num}->{all
} .= "\/$disks->{$num}->{path}";
787 $disks->{$num}->{last_part
} = $disk;
788 $disks->{$num}->{all
} .= "\/$disk";
793 die "ERROR: in path\n";
800 sub snapshot_destroy
{
801 my ($source, $dest, $method, $snap) = @_;
803 my @zfscmd = ('zfs', 'destroy');
804 my $snapshot = "$source->{all}\@$snap";
807 if($source->{ip
} && $method eq 'ssh'){
808 run_cmd
(['ssh', "root\@$source->{ip}", '--', @zfscmd, $snapshot]);
810 run_cmd
([@zfscmd, $snapshot]);
817 my @ssh = $dest->{ip
} ?
('ssh', "root\@$dest->{ip}", '--') : ();
819 my $path = "$dest->{all}\/$source->{last_part}";
822 run_cmd
([@ssh, @zfscmd, "$path\@$snap"]);
831 my ($source , $dest, $method) = @_;
834 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
835 push @$cmd, 'zfs', 'list', '-rt', 'snapshot', '-Ho', 'name';
836 push @$cmd, "$dest->{all}/$source->{last_part}\@$source->{old_snap}";
839 eval {$text =run_cmd
($cmd);};
845 while ($text && $text =~ s/^(.*?)(\n|$)//) {
847 return 1 if $line =~ m/^.*$source->{old_snap}$/;
852 my ($source, $dest, $param) = @_;
856 push @$cmd, 'ssh', "root\@$source->{ip}", '--' if $source->{ip
};
857 push @$cmd, 'zfs', 'send';
858 push @$cmd, '-v' if $param->{verbose
};
860 if($source->{last_snap
} && snapshot_exist
($source , $dest, $param->{method})) {
861 push @$cmd, '-i', "$source->{all}\@$source->{last_snap}";
863 push @$cmd, '--', "$source->{all}\@$source->{new_snap}";
865 if ($param->{limit
}){
866 my $bwl = $param->{limit
}*1024;
867 push @$cmd, \'|', 'cstream
', '-t
', $bwl;
869 my $target = "$dest->{all}/$source->{last_part}";
873 push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip
};
874 push @$cmd, 'zfs', 'recv', '-F', '--';
875 push @$cmd, "$target";
882 snapshot_destroy
($source, undef, $param->{method}, $source->{new_snap
});
889 my ($source, $dest, $method) = @_;
891 my $source_target = $source->{vm_type
} eq 'qemu' ?
"$QEMU_CONF/$source->{vmid}.conf": "$LXC_CONF/$source->{vmid}.conf";
892 my $dest_target_new ="$source->{vmid}.conf.$source->{vm_type}.$source->{new_snap}";
894 my $config_dir = $dest->{last_part
} ?
"${CONFIG_PATH}/$dest->{last_part}" : $CONFIG_PATH;
896 $dest_target_new = $config_dir.'/'.$dest_target_new;
898 if ($method eq 'ssh'){
899 if ($dest->{ip
} && $source->{ip
}) {
900 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
901 run_cmd
(['scp', '--', "root\@[$source->{ip}]:$source_target", "root\@[$dest->{ip}]:$dest_target_new"]);
902 } elsif ($dest->{ip
}) {
903 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $config_dir]);
904 run_cmd
(['scp', '--', $source_target, "root\@[$dest->{ip}]:$dest_target_new"]);
905 } elsif ($source->{ip
}) {
906 run_cmd
(['mkdir', '-p', '--', $config_dir]);
907 run_cmd
(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]);
910 if ($source->{destroy
}){
911 my $dest_target_old ="${config_dir}/$source->{vmid}.conf.$source->{vm_type}.$source->{old_snap}";
913 run_cmd
(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]);
915 run_cmd
(['rm', '-f', '--', $dest_target_old]);
918 } elsif ($method eq 'local') {
919 run_cmd
(['mkdir', '-p', '--', $config_dir]);
920 run_cmd
(['cp', $source_target, $dest_target_new]);
925 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
926 my $datestamp = sprintf ("%04d-%02d-%02d_%02d:%02d:%02d", $year+1900, $mon+1, $mday, $hour, $min, $sec);
932 my $cfg = read_cron
();
934 my $status_list = sprintf("%-25s%-15s%-10s\n", "SOURCE", "NAME", "STATUS");
936 my $states = read_state
();
938 foreach my $source (sort keys%{$cfg}) {
939 foreach my $sync_name (sort keys%{$cfg->{$source}}) {
940 $status_list .= sprintf("%-25s", cut_target_width
($source, 25));
941 $status_list .= sprintf("%-15s", cut_target_width
($sync_name, 25));
942 $status_list .= "$states->{$source}->{$sync_name}->{state}\n";
952 my $job = get_job
($param);
953 $job->{state} = "ok";
961 my $job = get_job
($param);
962 $job->{state} = "stopped";
967 my $command = $ARGV[0];
969 my $commands = {'destroy' => 1,
978 if (!$command || !$commands->{$command}) {
983 my $help_sync = "$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n
984 \twill sync one time\n
986 \t\tthe destination target is like [IP:]<Pool>[/Path]\n
988 \t\tmax sync speed in kBytes/s, default unlimited\n
989 \t-maxsnap\tinteger\n
990 \t\thow much snapshots will be kept before get erased, default 1/n
992 \t\tname of the sync job, if not set it is default.
993 \tIt is only necessary if scheduler allready contains this source.\n
995 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
997 my $help_create = "$PROGNAME create -dest <string> -source <string> [OPTIONS]/n
998 \tCreate a sync Job\n
1000 \t\tthe destination target is like [IP]:<Pool>[/Path]\n
1002 \t\tmax sync speed in kBytes/s, default unlimited\n
1003 \t-maxsnap\tstring\n
1004 \t\thow much snapshots will be kept before get erased, default 1\n
1006 \t\tname of the sync job, if not set it is default\n
1008 \t\tif this flag is set it will skip the first sync\n
1010 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1012 my $help_destroy = "$PROGNAME destroy -source <string> [OPTIONS]\n
1013 \tremove a sync Job from the scheduler\n
1015 \t\tname of the sync job, if not set it is default\n
1017 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1019 my $help_help = "$PROGNAME help <cmd> [OPTIONS]\n
1020 \tGet help about specified command.\n
1023 \t-verbose\tboolean\n
1024 \t\tVerbose output format.\n";
1026 my $help_list = "$PROGNAME list\n
1027 \tGet a List of all scheduled Sync Jobs\n";
1029 my $help_status = "$PROGNAME status\n
1030 \tGet the status of all scheduled Sync Jobs\n";
1032 my $help_enable = "$PROGNAME enable -source <string> [OPTIONS]\n
1033 \tenable a syncjob and reset error\n
1035 \t\tname of the sync job, if not set it is default\n
1037 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1039 my $help_disable = "$PROGNAME disable -source <string> [OPTIONS]\n
1042 \t\tname of the sync job, if not set it is default\n
1044 \t\tthe source can be an <VMID> or [IP:]<ZFSPool>[/Path]\n";
1060 die "$help_destroy\n";
1064 die "$help_create\n";
1072 die "$help_status\n";
1076 die "$help_enable\n";
1080 die "$help_enable\n";
1087 my $param = parse_argv
(@arg);
1093 die "$help_destroy\n" if !$param->{source
};
1094 check_target
($param->{source
});
1095 destroy_job
($param);
1099 die "$help_sync\n" if !$param->{source
} || !$param->{dest
};
1100 check_target
($param->{source
});
1101 check_target
($param->{dest
});
1106 die "$help_create\n" if !$param->{source
} || !$param->{dest
};
1107 check_target
($param->{source
});
1108 check_target
($param->{dest
});
1121 my $help_command = $ARGV[1];
1122 if ($help_command && $commands->{$help_command}) {
1123 print help
($help_command);
1125 if ($param->{verbose
} == 1){
1126 exec("man $PROGNAME");
1133 die "$help_enable\n" if !$param->{source
};
1134 check_target
($param->{source
});
1139 die "$help_disable\n" if !$param->{source
};
1140 check_target
($param->{source
});
1141 disable_job
($param);
1148 print("ERROR:\tno command specified\n") if !$help;
1149 print("USAGE:\t$PROGNAME <COMMAND> [ARGS] [OPTIONS]\n");
1150 print("\t$PROGNAME help [<cmd>] [OPTIONS]\n\n");
1151 print("\t$PROGNAME create -dest <string> -source <string> [OPTIONS]\n");
1152 print("\t$PROGNAME destroy -source <string> [OPTIONS]\n");
1153 print("\t$PROGNAME disable -source <string> [OPTIONS]\n");
1154 print("\t$PROGNAME enable -source <string> [OPTIONS]\n");
1155 print("\t$PROGNAME list\n");
1156 print("\t$PROGNAME status\n");
1157 print("\t$PROGNAME sync -dest <string> -source <string> [OPTIONS]\n");
1162 parse_target
($target);
1169 pve-zsync - PVE ZFS Replication Manager
1173 pve-zsync <COMMAND> [ARGS] [OPTIONS]
1175 pve-zsync help <cmd> [OPTIONS]
1177 Get help about specified command.
1185 Verbose output format.
1187 pve-zsync create -dest <string> -source <string> [OPTIONS]
1193 the destination target is like [IP]:<Pool>[/Path]
1197 max sync speed in kBytes/s, default unlimited
1201 how much snapshots will be kept before get erased, default 1
1205 name of the sync job, if not set it is default
1209 if this flag is set it will skip the first sync
1213 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1215 pve-zsync destroy -source <string> [OPTIONS]
1217 remove a sync Job from the scheduler
1221 name of the sync job, if not set it is default
1225 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1227 pve-zsync disable -source <string> [OPTIONS]
1233 name of the sync job, if not set it is default
1237 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1239 pve-zsync enable -source <string> [OPTIONS]
1241 enable a syncjob and reset error
1245 name of the sync job, if not set it is default
1249 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1252 Get a List of all scheduled Sync Jobs
1256 Get the status of all scheduled Sync Jobs
1258 pve-zsync sync -dest <string> -source <string> [OPTIONS]
1264 the destination target is like [IP:]<Pool>[/Path]
1268 max sync speed in kBytes/s, default unlimited
1272 how much snapshots will be kept before get erased, default 1
1276 name of the sync job, if not set it is default.
1277 It is only necessary if scheduler allready contains this source.
1281 the source can be an <VMID> or [IP:]<ZFSPool>[/Path]
1285 This Tool helps you to sync your VM or directory which stored on ZFS between 2 servers.
1286 This tool also has the capability to add jobs to cron so the sync will be automatically done.
1287 The default syncing interval is set to 15 min, if you want to change this value you can do this in /etc/cron.d/pve-zsync.
1288 To config cron see man crontab.
1290 =head2 PVE ZFS Storage sync Tool
1292 This Tool can get remote pool on other PVE or send Pool to others ZFS machines
1296 add sync job from local VM to remote ZFS Server
1297 pve-zsync create -source=100 -dest=192.168.1.2:zfspool
1299 =head1 IMPORTANT FILES
1301 Cron jobs and config are stored at /etc/cron.d/pve-zsync
1303 The VM config get copied on the destination machine to /var/lib/pve-zsync/
1305 =head1 COPYRIGHT AND DISCLAIMER
1307 Copyright (C) 2007-2015 Proxmox Server Solutions GmbH
1309 This program is free software: you can redistribute it and/or modify it
1310 under the terms of the GNU Affero General Public License as published
1311 by the Free Software Foundation, either version 3 of the License, or
1312 (at your option) any later version.
1314 This program is distributed in the hope that it will be useful, but
1315 WITHOUT ANY WARRANTY; without even the implied warranty of
1316 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1317 Affero General Public License for more details.
1319 You should have received a copy of the GNU Affero General Public
1320 License along with this program. If not, see
1321 <http://www.gnu.org/licenses/>.