]> git.proxmox.com Git - pve-zsync.git/blobdiff - pve-zsync
add disk parser for LXC
[pve-zsync.git] / pve-zsync
index ed65947b7c281849b979e19fa4f99a186f38e431..caf071274eacb1fc609692df2ae6e96ee49e8f48 100644 (file)
--- 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:]<VMID> or [IP:]<ZFSPool>[/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:]<ZFSPool>[/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