1 package PVE
::ReplicationTools
;
6 use PVE
::Tools
qw(run_command);
15 use Data
::Dumper
qw(Dumper);
17 my $STATE_DIR = '/var/lib/pve-replica';
18 my $STATE_PATH = "$STATE_DIR/pve-replica.state";
20 PVE
::Cluster
::cfs_update
;
21 my $local_node = PVE
::INotify
::nodename
();
25 my $get_guestconfig = sub {
28 my $vms = PVE
::Cluster
::get_vmlist
();
30 my $type = $vms->{ids
}->{$vmid}->{type
};
35 if ($type =~ m/^qemu$/) {
36 $guestconf = PVE
::QemuConfig-
>load_config($vmid);
37 $running = PVE
::QemuServer
::check_running
($vmid);
38 } elsif ($type =~ m/^lxc$/) {
39 $guestconf = PVE
::LXC
::Config-
>load_config($vmid);
40 $running = PVE
::LXC
::check_running
($vmid);
43 return ($guestconf, $type, $running);
51 PVE
::Tools
::file_set_contents
($STATE_PATH, JSON
::encode_json
($state));
56 return {} if !(-e
$STATE_PATH);
58 my $raw = PVE
::Tools
::file_get_contents
($STATE_PATH);
60 return {} if $raw eq '';
61 return JSON
::decode_json
($raw);
67 my $remoteip = PVE
::Cluster
::remote_node_ip
($nodename, 1);
69 my $dc_conf = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
70 if (my $network = $dc_conf->{storage_replication_network
}) {
72 my $cmd = ['ssh', '-o', 'Batchmode=yes', "root\@$remoteip", '--'
73 ,'pvecm', 'mtunnel', '--get_migration_ip',
74 '--migration_network', $network];
76 PVE
::Tools
::run_command
($cmd, outfunc
=> sub {
79 if ($line =~ m/^ip: '($PVE::Tools::IPRE)'$/) {
89 my $vms = PVE
::Cluster
::get_vmlist
();
91 my $state = read_state
();
95 foreach my $vmid (keys %{$vms->{ids
}}) {
96 next if $vms->{ids
}->{$vmid}->{node
} ne $local_node;
98 my $vm_state = $state->{$vmid};
99 next if !defined($vm_state);
103 $job->{limit
} = $vm_state->{limit
};
104 $job->{interval
} = $vm_state->{interval
};
105 $job->{tnode
} = $vm_state->{tnode
};
106 $job->{lastsync
} = $vm_state->{lastsync
};
107 $job->{state} = $vm_state->{state};
108 $job->{fail
} = $vm_state->{fail
};
110 $jobs->{$vmid} = $job;
117 my ($vmid, $param) = @_;
119 my $jobs = read_state
();
120 $jobs->{$vmid}->{state} = 'sync';
123 my ($guest_conf, $vm_type, $running) = &$get_guestconfig($vmid);
126 my $job = $jobs->{$vmid};
127 my $tnode = $job->{tnode
};
129 if ($vm_type eq "qemu" && defined($guest_conf->{agent
}) ) {
130 $qga = PVE
::QemuServer
::qga_check_running
($vmid)
134 # will not die if a disk is not syncable
135 my $disks = get_syncable_guestdisks
($guest_conf, $vm_type);
137 # check if all nodes have the storage availible
138 my $storage_config = PVE
::Storage
::config
();
139 foreach my $volid (keys %$disks) {
140 my ($storeid) = PVE
::Storage
::parse_volume_id
($volid);
142 my $store = $storage_config->{ids
}->{$storeid};
143 die "Storage $storeid not availible on node: $tnode\n"
144 if $store->{nodes
} && !$store->{nodes
}->{$tnode};
145 die "Storage $storeid not availible on node: $local_node\n"
146 if $store->{nodes
} && !$store->{nodes
}->{$local_node};
150 my $limit = $param->{limit
};
151 $limit = $guest_conf->{replica_rate_limit
}
152 if (!defined($limit));
154 my $snap_time = time();
156 die "Invalid synctime format: $job->{lastsync}."
157 if $job->{lastsync
} !~ m/^(\d+)$/;
160 my $incremental_snap = $lastsync ?
"replica_$lastsync" : undef;
162 # freeze filesystem for data consistency
164 print "Freeze guest filesystem\n";
167 PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-freeze");
171 my $snapname = "replica_$snap_time";
173 my $disks_status = { snapname
=> $snapname };
177 # make snapshot of all volumes
178 foreach my $volid (keys %$disks) {
181 PVE
::Storage
::volume_snapshot
($storage_config, $volid, $snapname);
186 print "Unfreeze guest filesystem\n";
188 PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-thaw");
192 cleanup_snapshot
($disks_status, $snapname, $storage_config, $running);
193 $jobs->{$vmid}->{state} = 'error';
199 $disks_status->{$volid}->{snapshot
} = 1;
203 print "Unfreeze guest filesystem\n";
204 eval { PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-thaw"); };
208 my $ip = get_node_ip
($tnode);
210 foreach my $volid (keys %$disks) {
213 PVE
::Storage
::volume_send
($storage_config, $volid, $snapname,
214 $ip, $incremental_snap,
215 $param->{verbose
}, $limit);
220 cleanup_snapshot
($disks_status, $snapname, $storage_config, $running, $ip);
222 $job->{state} = 'error' if $job->{fail
} > 3;
224 $jobs->{$vmid} = $job;
229 $disks_status->{$volid}->{synced
} = 1;
232 # delet old snapshot if exists
233 cleanup_snapshot
($disks_status, $snapname, $storage_config, $running, $ip, $lastsync) if
234 $job->{lastsync
} ne '0';
236 $job->{lastsync
} = $snap_time;
237 $job->{state} = "ok";
238 $jobs->{$vmid} = $job;
242 PVE
::Tools
::lock_file_full
($STATE_PATH, 60, 0 , $sync_job);
249 my ($vol, $prefix, $nodes) = @_;
251 my $plugin = $vol->{plugin
};
252 return $plugin->get_snapshots($vol, $prefix, $nodes);
256 my ($vol, $param, $ip, $all_snaps_in_delta, $alter_path) = @_;
258 my $plugin = $vol->{plugin
};
259 $plugin->send_image($vol, $param, $ip, $all_snaps_in_delta, $alter_path);
263 my ($vmid, $no_sync, $target) = @_;
265 my $update_state = sub {
268 my $jobs = read_state
();
269 my $job = $jobs->{$vmid};
270 my ($config) = &$get_guestconfig($vmid);
273 $job->{interval
} = $config->{replica_interval
} || 15;
275 $job->{tnode
} = $target || $config->{replica_target
};
276 die "Replication target must be set\n" if !defined($job->{tnode
});
278 die "Target and source node can't be the same\n"
279 if $job->{tnode
} eq $local_node;
282 if (!defined($job->{lastsync
})) {
284 if ( my $lastsync = get_lastsync
($vmid)) {
285 $job->{lastsync
} = $lastsync;
287 $job->{lastsync
} = 0;
291 $param->{verbose
} = 1;
293 $job->{state} = 'ok';
294 $jobs->{$vmid} = $job;
298 sync_guest
($vmid, $param) if !defined($no_sync);
301 $jobs->{$vmid}->{state} = 'error';
307 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
314 my $update_state = sub {
316 my $jobs = read_state
();
318 if (defined($jobs->{$vmid})) {
319 $jobs->{$vmid}->{state} = 'off';
322 print "No replica service for $vmid\n";
326 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
333 my $update_state = sub {
335 my $jobs = read_state
();
337 if (defined($jobs->{$vmid})) {
338 delete($jobs->{$vmid});
341 print "No replica service for $vmid\n";
345 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
349 sub get_syncable_guestdisks
{
350 my ($config, $vm_type, $running, $noerr) = @_;
352 my $syncable_disks = {};
354 my $cfg = PVE
::Storage
::config
();
358 my ($id, $volume) = @_;
359 return if !defined($volume->{replica
}) || !$volume->{replica
};
362 if ($vm_type eq 'qemu') {
363 $volname = $volume->{file
};
365 $volname = $volume->{volume
};
368 if( PVE
::Storage
::volume_has_feature
($cfg, 'replicate', $volname , undef, $running)) {
369 $syncable_disks->{$volname} = 1;
371 warn "Can't sync Volume: $volname\n" if !$noerr;
376 if ($vm_type eq 'qemu') {
377 PVE
::QemuServer
::foreach_drive
($config, $func);
378 } elsif ($vm_type eq 'lxc') {
379 PVE
::LXC
::Config-
>foreach_mountpoint($config, $func);
381 die "Unknown VM type: $vm_type";
384 return wantarray ?
($warnings, $syncable_disks) : $syncable_disks;
387 sub destroy_all_snapshots
{
388 my ($vmid, $regex, $node) = @_;
390 my $ip = defined($node) ? get_node_ip
($node) : undef;
392 my ($guest_conf, $vm_type, $running) = &$get_guestconfig($vmid);
394 my $disks = get_syncable_guestdisks
($guest_conf, $vm_type);
395 my $cfg = PVE
::Storage
::config
();
398 foreach my $volid (keys %$disks) {
399 $snapshots->{$volid} =
400 PVE
::Storage
::volume_snapshot_list
($cfg, $volid, $regex, $node, $ip);
403 foreach my $volid (keys %$snapshots) {
405 if (defined($regex)) {
406 foreach my $snap (@{$snapshots->{$volid}}) {
408 PVE
::Storage
::volume_snapshot_delete_remote
($cfg, $volid, $snap, $ip);
410 PVE
::Storage
::volume_snapshot_delete
($cfg, $volid, $snap, $running);
416 my $cmd = ['ssh', '-o', 'Batchmode=yes', "root\@$ip", '--'
417 ,'pvesm', 'free', $volid];
418 PVE
::Tools
::run_command
($cmd);
420 PVE
::Storage
::vdisk_free
($cfg, $volid);
427 sub cleanup_snapshot
{
428 my ($disks, $snapname, $cfg, $running, $ip, $lastsync_snap) = @_;
430 if ($lastsync_snap) {
431 $snapname = "replica_$lastsync_snap";
434 foreach my $volid (keys %$disks) {
435 next if $volid eq "snapname";
437 if (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);