1 package PVE
::ReplicationTools
;
17 my $STATE_DIR = '/var/lib/pve-replica';
18 my $STATE_PATH = "$STATE_DIR/pve-replica.state";
20 my $local_node = PVE
::INotify
::nodename
();
22 my $get_ssh_cmd = sub {
25 return ['ssh', '-o', 'Batchmode=yes', "root\@$ip" ];
28 my $get_guestconfig = sub {
31 my $vms = PVE
::Cluster
::get_vmlist
();
33 my $type = $vms->{ids
}->{$vmid}->{type
};
38 if ($type eq 'qemu') {
39 $guestconf = PVE
::QemuConfig-
>load_config($vmid);
40 $running = PVE
::QemuServer
::check_running
($vmid);
41 } elsif ($type eq 'lxc') {
42 $guestconf = PVE
::LXC
::Config-
>load_config($vmid);
43 $running = PVE
::LXC
::check_running
($vmid);
48 return ($guestconf, $type, $running);
56 PVE
::Tools
::file_set_contents
($STATE_PATH, encode_json
($state));
61 return {} if ! -e
$STATE_PATH;
63 my $raw = PVE
::Tools
::file_get_contents
($STATE_PATH);
65 return {} if $raw eq '';
67 return decode_json
($raw);
73 my $remoteip = PVE
::Cluster
::remote_node_ip
($nodename, 1);
75 my $dc_conf = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
76 if (my $network = $dc_conf->{storage_replication_network
}) {
78 my $cmd = $get_ssh_cmd->($remoteip);
80 push @$cmd, '--', 'pvecm', 'mtunnel', '--get_migration_ip', '--migration_network', $network;
82 PVE
::Tools
::run_command
($cmd, outfunc
=> sub {
85 if ($line =~ m/^ip: '($PVE::Tools::IPRE)'$/) {
95 my $vms = PVE
::Cluster
::get_vmlist
();
97 my $state = read_state
();
101 foreach my $vmid (keys %{$vms->{ids
}}) {
102 next if $vms->{ids
}->{$vmid}->{node
} ne $local_node;
104 my $vm_state = $state->{$vmid};
105 next if !defined($vm_state);
109 $job->{limit
} = $vm_state->{limit
};
110 $job->{interval
} = $vm_state->{interval
};
111 $job->{tnode
} = $vm_state->{tnode
};
112 $job->{lastsync
} = $vm_state->{lastsync
};
113 $job->{state} = $vm_state->{state};
114 $job->{fail
} = $vm_state->{fail
};
116 $jobs->{$vmid} = $job;
123 my ($vmid, $param) = @_;
125 my $jobs = read_state
();
126 $jobs->{$vmid}->{state} = 'sync';
129 my ($guest_conf, $vm_type, $running) = &$get_guestconfig($vmid);
132 my $job = $jobs->{$vmid};
133 my $tnode = $job->{tnode
};
135 if ($vm_type eq 'qemu' && defined($guest_conf->{agent
}) ) {
136 $qga = PVE
::QemuServer
::qga_check_running
($vmid)
140 # will not die if a disk is not syncable
141 my $disks = get_syncable_guestdisks
($guest_conf, $vm_type);
143 # check if all nodes have the storage availible
144 my $storage_config = PVE
::Storage
::config
();
145 foreach my $volid (keys %$disks) {
146 my ($storeid) = PVE
::Storage
::parse_volume_id
($volid);
148 my $store = $storage_config->{ids
}->{$storeid};
149 die "Storage $storeid not availible on node: $tnode\n"
150 if $store->{nodes
} && !$store->{nodes
}->{$tnode};
151 die "Storage $storeid not availible on node: $local_node\n"
152 if $store->{nodes
} && !$store->{nodes
}->{$local_node};
156 my $limit = $param->{limit
};
157 $limit = $guest_conf->{replica_rate_limit
}
158 if (!defined($limit));
160 my $snap_time = time();
162 die "Invalid synctime format: $job->{lastsync}."
163 if $job->{lastsync
} !~ m/^(\d+)$/;
166 my $incremental_snap = $lastsync ?
"replica_$lastsync" : undef;
168 # freeze filesystem for data consistency
170 print "Freeze guest filesystem\n";
173 PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-freeze");
177 my $snapname = "replica_$snap_time";
179 my $disks_status = {};
183 # make snapshot of all volumes
184 foreach my $volid (keys %$disks) {
187 PVE
::Storage
::volume_snapshot
($storage_config, $volid, $snapname);
192 print "Unfreeze guest filesystem\n";
194 PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-thaw");
198 cleanup_snapshot
($disks_status, $snapname, $storage_config, $running);
199 $jobs->{$vmid}->{state} = 'error';
205 $disks_status->{$volid}->{snapshot
} = 1;
209 print "Unfreeze guest filesystem\n";
210 eval { PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-thaw"); };
214 my $ip = get_node_ip
($tnode);
216 foreach my $volid (keys %$disks) {
219 PVE
::Storage
::volume_send
($storage_config, $volid, $snapname,
220 $ip, $incremental_snap,
221 $param->{verbose
}, $limit);
226 cleanup_snapshot
($disks_status, $snapname, $storage_config, $running, $ip);
228 $job->{state} = 'error' if $job->{fail
} > 3;
230 $jobs->{$vmid} = $job;
235 $disks_status->{$volid}->{synced
} = 1;
238 # delet old snapshot if exists
239 cleanup_snapshot
($disks_status, $snapname, $storage_config, $running, $ip, $lastsync) if
242 $job->{lastsync
} = $snap_time;
243 $job->{state} = "ok";
244 $jobs->{$vmid} = $job;
248 PVE
::Tools
::lock_file_full
($STATE_PATH, 60, 0 , $sync_job);
255 my ($vol, $param, $ip, $all_snaps_in_delta, $alter_path) = @_;
257 my $plugin = $vol->{plugin
};
258 $plugin->send_image($vol, $param, $ip, $all_snaps_in_delta, $alter_path);
262 my ($vmid, $no_sync, $target) = @_;
264 my $update_state = sub {
267 my $jobs = read_state
();
268 my $job = $jobs->{$vmid};
269 my ($config) = &$get_guestconfig($vmid);
272 $job->{interval
} = $config->{replica_interval
} || 15;
274 $job->{tnode
} = $target || $config->{replica_target
};
275 die "Replication target must be set\n" if !defined($job->{tnode
});
277 die "Target and source node can't be the same\n"
278 if $job->{tnode
} eq $local_node;
281 if (!defined($job->{lastsync
})) {
283 if ( my $lastsync = get_lastsync
($vmid)) {
284 $job->{lastsync
} = $lastsync;
286 $job->{lastsync
} = 0;
290 $param->{verbose
} = 1;
292 $job->{state} = 'ok';
293 $jobs->{$vmid} = $job;
297 sync_guest
($vmid, $param) if !defined($no_sync);
300 $jobs->{$vmid}->{state} = 'error';
306 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
313 my $update_state = sub {
315 my $jobs = read_state
();
317 if (defined($jobs->{$vmid})) {
318 $jobs->{$vmid}->{state} = 'off';
321 print "No replica service for $vmid\n";
325 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
332 my $update_state = sub {
334 my $jobs = read_state
();
336 if (defined($jobs->{$vmid})) {
337 delete($jobs->{$vmid});
340 print "No replica service for $vmid\n";
344 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
348 sub get_syncable_guestdisks
{
349 my ($config, $vm_type, $running, $noerr) = @_;
351 my $syncable_disks = {};
353 my $cfg = PVE
::Storage
::config
();
357 my ($id, $volume) = @_;
358 return if !defined($volume->{replica
}) || !$volume->{replica
};
361 if ($vm_type eq 'qemu') {
362 $volname = $volume->{file
};
364 $volname = $volume->{volume
};
367 if( PVE
::Storage
::volume_has_feature
($cfg, 'replicate', $volname , undef, $running)) {
368 $syncable_disks->{$volname} = 1;
370 warn "Can't sync Volume: $volname\n" if !$noerr;
375 if ($vm_type eq 'qemu') {
376 PVE
::QemuServer
::foreach_drive
($config, $func);
377 } elsif ($vm_type eq 'lxc') {
378 PVE
::LXC
::Config-
>foreach_mountpoint($config, $func);
380 die "Unknown VM type: $vm_type";
383 return wantarray ?
($warnings, $syncable_disks) : $syncable_disks;
386 sub destroy_all_snapshots
{
387 my ($vmid, $regex, $node) = @_;
389 my $ip = defined($node) ? get_node_ip
($node) : undef;
391 my ($guest_conf, $vm_type, $running) = &$get_guestconfig($vmid);
393 my $disks = get_syncable_guestdisks
($guest_conf, $vm_type);
394 my $cfg = PVE
::Storage
::config
();
397 foreach my $volid (keys %$disks) {
398 $snapshots->{$volid} =
399 PVE
::Storage
::volume_snapshot_list
($cfg, $volid, $regex, $node, $ip);
402 foreach my $volid (keys %$snapshots) {
404 if (defined($regex)) {
405 foreach my $snap (@{$snapshots->{$volid}}) {
407 PVE
::Storage
::volume_snapshot_delete_remote
($cfg, $volid, $snap, $ip);
409 PVE
::Storage
::volume_snapshot_delete
($cfg, $volid, $snap, $running);
415 my $cmd = $get_ssh_cmd->($ip);
417 push @$cmd, '--', 'pvesm', 'free', $volid;
419 PVE
::Tools
::run_command
($cmd);
421 PVE
::Storage
::vdisk_free
($cfg, $volid);
428 sub cleanup_snapshot
{
429 my ($disks, $snapname, $cfg, $running, $ip, $lastsync_snap) = @_;
431 if ($lastsync_snap) {
432 $snapname = "replica_$lastsync_snap";
435 foreach my $volid (keys %$disks) {
437 if (defined($ip) && (defined($lastsync_snap) || $disks->{$volid}->{synced
})) {
438 PVE
::Storage
::volume_snapshot_delete_remote
($cfg, $volid, $snapname, $ip);
441 if (defined($lastsync_snap) || $disks->{$volid}->{snapshot
}) {
442 PVE
::Storage
::volume_snapshot_delete
($cfg, $volid, $snapname, $running);
447 sub destroy_replica
{
452 my $jobs = read_state
();
454 return if !defined($jobs->{$vmid});
456 my ($guest_conf, $vm_type) = &$get_guestconfig($vmid);
458 destroy_all_snapshots
($vmid, 'replica');
459 destroy_all_snapshots
($vmid, undef, $guest_conf->{replica_target
});
461 delete($jobs->{$vmid});
463 delete($guest_conf->{replica_rate_limit
});
464 delete($guest_conf->{replica_rate_interval
});
465 delete($guest_conf->{replica_target
});
466 delete($guest_conf->{replica
});
468 if ($vm_type eq 'qemu') {
469 PVE
::QemuConfig-
>write_config($vmid, $guest_conf);
471 PVE
::LXC
::Config-
>write_config($vmid, $guest_conf);
476 PVE
::Tools
::lock_file_full
($STATE_PATH, 30, 0 , $code);
483 my ($conf, $vm_type) = &$get_guestconfig($vmid);
485 my $sync_vol = get_syncable_guestdisks
($conf, $vm_type);
486 my $cfg = PVE
::Storage
::config
();
489 foreach my $volid (keys %$sync_vol) {
491 PVE
::Storage
::volume_snapshot_list
($cfg, $volid, 'replica', $local_node);
493 if (my $tmp_snap = shift @$list) {
494 $tmp_snap =~ m/^replica_(\d+)$/;
495 die "snapshots are not coherent\n"
496 if defined($time) && !($time eq $1);
504 sub get_last_replica_snap
{
507 my $cfg = PVE
::Storage
::config
();
508 my $list = PVE
::Storage
::volume_snapshot_list
($cfg, $volid, 'replica_', $local_node);
513 sub check_guest_volumes_syncable
{
514 my ($conf, $vm_type) = @_;
516 my ($warnings, $disks) = get_syncable_guestdisks
($conf, $vm_type, 1);
518 return undef if $warnings || !%$disks;
524 my ($vmid, $key, $value) = @_;
526 if ($key eq 'replica_target') {
527 destroy_replica
($vmid);
528 job_enable
($vmid, undef, $value);
533 my $jobs = read_state
();
535 return if !defined($jobs->{$vmid});
537 if ($key eq 'replica_interval') {
538 $jobs->{$vmid}->{interval
} = $value || 15;
539 } elsif ($key eq 'replica_rate_limit'){
540 $jobs->{$vmid}->{limit
} = $value ||
541 delet
$jobs->{$vmid}->{limit
};
543 die "Config parameter $key not known";
549 PVE
::Tools
::lock_file_full
($STATE_PATH, 60, 0 , $update);