X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=pve-zsync;h=caf071274eacb1fc609692df2ae6e96ee49e8f48;hb=9e7685c240a6996e60c4c4e0f7f20ee7ddd0dfc0;hp=ed65947b7c281849b979e19fa4f99a186f38e431;hpb=a018f134d618ba9bfa0b46f8ef55faab39780802;p=pve-zsync.git diff --git a/pve-zsync b/pve-zsync index ed65947..caf0712 100644 --- a/pve-zsync +++ b/pve-zsync @@ -6,21 +6,47 @@ use Data::Dumper qw(Dumper); use Fcntl qw(:flock SEEK_END); use Getopt::Long qw(GetOptionsFromArray); use File::Copy qw(move); +use File::Path qw(make_path); use Switch; 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 $CRONJOBS = "/etc/cron.d/$PROGNAME"; my $PATH = "/usr/sbin/"; -my $QEMU_CONF = "/etc/pve/local/qemu-server/"; +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'); @@ -77,12 +103,12 @@ 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 = []; + push @$cmd, 'ssh', "root\@$target->{ip}", '--', if $target->{ip}; + push @$cmd, 'zfs', 'list', '-H', '--', $target->{all}; eval { run_cmd($cmd); }; @@ -99,20 +125,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); - die "$text $errstr\n" if !($target->{pool} = shift(@parts)); - die "$text $errstr\n" if $target->{pool} =~ /^(\d+.\d+.\d+.\d+)$/; + $target->{ip} =~ s/^\[(.*)\]$/$1/ if $target->{ip}; - if ($target->{pool} =~ m/^\d+$/) { - $target->{vmid} = $target->{pool}; + my $pool = $target->{pool} = shift(@parts); + die "$errstr\n" if !$pool; + + if ($pool =~ m/^\d+$/) { + $target->{vmid} = $pool; delete $target->{pool}; } @@ -191,6 +217,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,6 +271,7 @@ 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 "{}"; @@ -288,6 +316,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"}; @@ -386,15 +415,16 @@ 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%-10s%-7s%-20s%-5s%-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("%-10s", cut_target_width($name, 10)); $list .= sprintf("%-7s", $states->{$source}->{$name}->{state}); $list .= sprintf("%-20s",$states->{$source}->{$name}->{lsync}); + $list .= sprintf("%-5s",$states->{$source}->{$name}->{vm_type}); $list .= sprintf("%-5s\n",$cfg->{$source}->{$name}->{method}); } } @@ -404,14 +434,19 @@ sub list { sub vm_exists { my ($target) = @_; + + my @cmd = ('ssh', "root\@$target->{ip}", '--') if $target->{ip}; - my $cmd = ""; - $cmd = "ssh root\@$target->{ip} " if ($target->{ip}); - $cmd .= "qm status $target->{vmid}"; + my $res = undef; - my $res = run_cmd($cmd); + 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; } @@ -429,20 +464,23 @@ 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}; + my $check = check_pool_exists($source->{path}, $source->{ip}) if !$source->{vmid} && $source->{path}; die "Pool $source->{path} does not exists\n" if undef($check); - die "VM $source->{vmid} doesn't exist\n" if $param->{vmid} && !vm_exists($source); + my $vm_type = vm_exists($source); + $job->{vm_type} = $vm_type; + + die "VM $source->{vmid} doesn't exist\n" if $param->{vmid} && !$vm_type; die "Config already exists\n" if $cfg->{$job->{source}}->{$job->{name}}; @@ -517,14 +555,18 @@ sub sync { }; + my $vm_type = vm_exists($source); + $source->{vm_type} = $vm_type; + if ($job) { $job->{state} = "syncing"; + $job->{vm_type} = $vm_type if !$job->{vm_type}; update_state($job); } eval{ if ($source->{vmid}) { - die "VM $source->{vmid} doesn't exist\n" if !vm_exists($source); + die "VM $source->{vmid} doesn't exist\n" if !$vm_type; my $disks = get_disks($source); foreach my $disk (sort keys %{$disks}) { @@ -565,10 +607,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; @@ -603,9 +645,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); }; @@ -655,13 +697,20 @@ sub write_cron { sub get_disks { my ($target) = @_; - 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}); return $disks; } @@ -670,6 +719,9 @@ 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 "COMMAND:\n\t$cmd\nGET ERROR:\n\t$output" if 0 != $?; @@ -688,48 +740,54 @@ sub parse_disks { my $num = 0; while ($text && $text =~ s/^(.*?)(\n|$)//) { my $line = $1; + + next if $line =~ /cdrom|none/; + next if $line !~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): /; + 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\-]+),(.*)$/) { + if($line =~ m/^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): (.+:)([A-Za-z0-9\-]+),(.*)$/) { $disk = $3; $stor = $2; + } else { + die "disk is not on ZFS Storage\n"; } - die "disk is not on ZFS Storage\n" if $is_disk && !$disk && $line !~ m/cdrom/; + my $cmd = []; + push @$cmd, 'ssh', "root\@$ip", '--' if $ip; + push @$cmd, 'pvesm', 'path', "$stor$disk"; + my $path = run_cmd($cmd); - 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); + if ($vm_type eq 'qemu' && $path =~ m/^\/dev\/zvol\/(\w+.*)(\/$disk)$/) { - if ($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} = $2; + $disks->{$num}->{all} .= "\/$disks->{$num}->{path}"; } + + $disks->{$num}->{last_part} = $disk; + $disks->{$num}->{all} .= "\/$disk"; + + $num++; + + } else { + die "ERROR: in path\n"; } } @@ -739,26 +797,26 @@ sub parse_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"; @@ -769,10 +827,10 @@ sub snapshot_destroy { 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->{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);}; @@ -790,26 +848,28 @@ 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', "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->{last_snap} $source->{all}\@$source->{new_snap} "; - } else { - $cmd .= "$source->{all}\@$source->{new_snap} "; + 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; + + push @$cmd, \'|'; + push @$cmd, 'ssh', "root\@$dest->{ip}", '--' if $dest->{ip}; + push @$cmd, 'zfs', 'recv', '-F', '--'; + push @$cmd, "$target"; eval { run_cmd($cmd) @@ -825,27 +885,27 @@ sub send_image { sub send_config{ my ($source, $dest, $method) = @_; - my $source_target ="$QEMU_CONF$source->{vmid}.conf"; + my $source_target = $source->{vm_type} eq 'qemu' ? "$QEMU_CONF$source->{vmid}.conf": "$LXC_CONF$source->{vmid}.conf"; my $dest_target_new ="$CONFIG_PATH$source->{vmid}.conf.$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"); + run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]); + 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"); + run_cmd(['ssh', "root\@$dest->{ip}", '--', 'mkdir', '-p', '--', $CONFIG_PATH]); + 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"); + run_cmd(['mkdir', '-p', '--', $CONFIG_PATH]); + run_cmd(['scp', '--', "root\@$source->{ip}:$source_target", $dest_target_new]); } 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"); + run_cmd(['ssh', "root\@$dest->{ip}", '--', 'rm', '-f', '--', $dest_target_old]); } else { - run_cmd("rm -f $dest_target_old"); + run_cmd(['rm', '-f', '--', $dest_target_old]); } } } @@ -1089,14 +1149,7 @@ sub usage { 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; + parse_target($target); } __END__ @@ -1235,11 +1288,9 @@ 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