1 package PVE
::ReplicationTools
;
17 my $STATE_DIR = '/var/lib/pve-replica';
18 my $STATE_PATH = "$STATE_DIR/pve-replica.state";
20 my $get_ssh_cmd = sub {
23 return ['ssh', '-o', 'Batchmode=yes', "root\@$ip" ];
26 sub get_guest_config
{
29 my $vms = PVE
::Cluster
::get_vmlist
();
31 die "no such guest '$vmid'\n" if !defined($vms->{ids
}->{$vmid});
33 my $vm_type = $vms->{ids
}->{$vmid}->{type
};
38 if ($vm_type eq 'qemu') {
39 $conf = PVE
::QemuConfig-
>load_config($vmid);
40 $running = PVE
::QemuServer
::check_running
($vmid);
41 } elsif ($vm_type eq 'lxc') {
42 $conf = PVE
::LXC
::Config-
>load_config($vmid);
43 $running = PVE
::LXC
::check_running
($vmid);
48 return ($conf, $vm_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);
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 my $local_node = PVE
::INotify
::nodename
();
103 foreach my $vmid (keys %{$vms->{ids
}}) {
104 next if $vms->{ids
}->{$vmid}->{node
} ne $local_node;
105 my $vm_state = $state->{$vmid};
106 next if !defined($vm_state);
110 $job->{limit
} = $vm_state->{limit
};
111 $job->{interval
} = $vm_state->{interval
};
112 $job->{tnode
} = $vm_state->{tnode
};
113 $job->{lastsync
} = $vm_state->{lastsync
};
114 $job->{state} = $vm_state->{state};
115 $job->{fail
} = $vm_state->{fail
};
117 $jobs->{$vmid} = $job;
124 my ($vmid, $param) = @_;
126 my $local_node = PVE
::INotify
::nodename
();
128 my $jobs = read_state
();
129 $jobs->{$vmid}->{state} = 'sync';
132 my ($guest_conf, $vm_type, $running) = get_guest_config
($vmid);
135 my $job = $jobs->{$vmid};
136 my $tnode = $job->{tnode
};
138 if ($vm_type eq 'qemu' && defined($guest_conf->{agent
}) ) {
139 $qga = PVE
::QemuServer
::qga_check_running
($vmid)
143 my $storecfg = PVE
::Storage
::config
();
144 # will not die if a disk is not syncable
145 my $disks = get_replicatable_volumes
($storecfg, $guest_conf, $vm_type);
147 # check if all nodes have the storage availible
148 foreach my $volid (keys %$disks) {
149 my ($storeid) = PVE
::Storage
::parse_volume_id
($volid);
151 my $store = $storecfg->{ids
}->{$storeid};
152 die "Storage $storeid not availible on node: $tnode\n"
153 if $store->{nodes
} && !$store->{nodes
}->{$tnode};
154 die "Storage $storeid not availible on node: $local_node\n"
155 if $store->{nodes
} && !$store->{nodes
}->{$local_node};
159 my $limit = $param->{limit
};
160 $limit = $guest_conf->{replica_rate_limit
}
161 if (!defined($limit));
163 my $snap_time = time();
165 die "Invalid synctime format: $job->{lastsync}."
166 if $job->{lastsync
} !~ m/^(\d+)$/;
169 my $incremental_snap = $lastsync ?
"replica_$lastsync" : undef;
171 # freeze filesystem for data consistency
173 print "Freeze guest filesystem\n";
176 PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-freeze");
180 my $snapname = "replica_$snap_time";
182 my $disks_status = {};
186 # make snapshot of all volumes
187 foreach my $volid (keys %$disks) {
190 PVE
::Storage
::volume_snapshot
($storecfg, $volid, $snapname);
195 print "Unfreeze guest filesystem\n";
197 PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-thaw");
201 cleanup_snapshot
($disks_status, $snapname, $storecfg, $running);
202 $jobs->{$vmid}->{state} = 'error';
208 $disks_status->{$volid}->{snapshot
} = 1;
212 print "Unfreeze guest filesystem\n";
213 eval { PVE
::QemuServer
::vm_mon_cmd
($vmid, "guest-fsfreeze-thaw"); };
217 my $ip = get_node_ip
($tnode);
219 foreach my $volid (keys %$disks) {
222 PVE
::Storage
::volume_send
($storecfg, $volid, $snapname,
223 $ip, $incremental_snap,
224 $param->{verbose
}, $limit);
229 cleanup_snapshot
($disks_status, $snapname, $storecfg, $running, $ip);
231 $job->{state} = 'error' if $job->{fail
} > 3;
233 $jobs->{$vmid} = $job;
238 $disks_status->{$volid}->{synced
} = 1;
241 # delete old snapshot if exists
242 cleanup_snapshot
($disks_status, $snapname, $storecfg, $running, $ip, $lastsync) if
245 $job->{lastsync
} = $snap_time;
246 $job->{state} = "ok";
247 $jobs->{$vmid} = $job;
251 PVE
::Tools
::lock_file_full
($STATE_PATH, 60, 0 , $sync_job);
258 my ($vol, $param, $ip, $all_snaps_in_delta, $alter_path) = @_;
260 my $plugin = $vol->{plugin
};
261 $plugin->send_image($vol, $param, $ip, $all_snaps_in_delta, $alter_path);
265 my ($vmid, $no_sync, $target) = @_;
267 my $local_node = PVE
::INotify
::nodename
();
269 my $update_state = sub {
272 my $jobs = read_state
();
273 my $job = $jobs->{$vmid};
274 my ($config) = get_guest_config
($vmid);
277 $job->{interval
} = $config->{replica_interval
} || 15;
279 $job->{tnode
} = $target || $config->{replica_target
};
280 die "Replication target must be set\n" if !defined($job->{tnode
});
282 die "Target and source node can't be the same\n"
283 if $job->{tnode
} eq $local_node;
286 if (!defined($job->{lastsync
})) {
288 if ( my $lastsync = get_lastsync
($vmid)) {
289 $job->{lastsync
} = $lastsync;
291 $job->{lastsync
} = 0;
295 $param->{verbose
} = 1;
297 $job->{state} = 'ok';
298 $jobs->{$vmid} = $job;
302 sync_guest
($vmid, $param) if !defined($no_sync);
305 $jobs->{$vmid}->{state} = 'error';
311 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
318 my $update_state = sub {
320 my $jobs = read_state
();
322 if (defined($jobs->{$vmid})) {
323 $jobs->{$vmid}->{state} = 'off';
326 print "No replica service for $vmid\n";
330 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
337 my $update_state = sub {
339 my $jobs = read_state
();
341 if (defined($jobs->{$vmid})) {
342 delete($jobs->{$vmid});
345 print "No replica service for $vmid\n";
349 PVE
::Tools
::lock_file_full
($STATE_PATH, 5, 0 , $update_state);
353 sub get_replicatable_volumes
{
354 my ($storecfg, $conf, $vm_type, $noerr) = @_;
356 if ($vm_type eq 'qemu') {
357 PVE
::QemuConfig-
>get_replicatable_volumes($storecfg, $conf, $noerr);
358 } elsif ($vm_type eq 'lxc') {
359 PVE
::LXC
::Config-
>get_replicatable_volumes($storecfg, $conf, $noerr);
361 die "internal error";
365 sub destroy_all_snapshots
{
366 my ($vmid, $regex, $node) = @_;
368 my $ip = defined($node) ? get_node_ip
($node) : undef;
370 my ($guest_conf, $vm_type, $running) = get_guest_config
($vmid);
372 my $storecfg = PVE
::Storage
::config
();
373 my $disks = get_replicatable_volumes
($storecfg, $guest_conf, $vm_type);
376 foreach my $volid (keys %$disks) {
377 $snapshots->{$volid} =
378 PVE
::Storage
::volume_snapshot_list
($storecfg, $volid, $regex, $node, $ip);
381 foreach my $volid (keys %$snapshots) {
383 if (defined($regex)) {
384 foreach my $snap (@{$snapshots->{$volid}}) {
386 PVE
::Storage
::volume_snapshot_delete_remote
($storecfg, $volid, $snap, $ip);
388 PVE
::Storage
::volume_snapshot_delete
($storecfg, $volid, $snap, $running);
394 my $cmd = $get_ssh_cmd->($ip);
396 push @$cmd, '--', 'pvesm', 'free', $volid;
398 PVE
::Tools
::run_command
($cmd);
400 die "internal error";
407 sub cleanup_snapshot
{
408 my ($disks, $snapname, $storecfg, $running, $ip, $lastsync_snap) = @_;
410 if ($lastsync_snap) {
411 $snapname = "replica_$lastsync_snap";
414 foreach my $volid (keys %$disks) {
416 if (defined($ip) && (defined($lastsync_snap) || $disks->{$volid}->{synced
})) {
417 PVE
::Storage
::volume_snapshot_delete_remote
($storecfg, $volid, $snapname, $ip);
420 if (defined($lastsync_snap) || $disks->{$volid}->{snapshot
}) {
421 PVE
::Storage
::volume_snapshot_delete
($storecfg, $volid, $snapname, $running);
426 sub destroy_replica
{
431 my $jobs = read_state
();
433 return if !defined($jobs->{$vmid});
435 my ($guest_conf, $vm_type) = get_guest_config
($vmid);
437 destroy_all_snapshots
($vmid, 'replica_');
438 destroy_all_snapshots
($vmid, undef, $guest_conf->{replica_target
});
440 delete($jobs->{$vmid});
442 delete($guest_conf->{replica_rate_limit
});
443 delete($guest_conf->{replica_rate_interval
});
444 delete($guest_conf->{replica_target
});
445 delete($guest_conf->{replica
});
447 if ($vm_type eq 'qemu') {
448 PVE
::QemuConfig-
>write_config($vmid, $guest_conf);
450 PVE
::LXC
::Config-
>write_config($vmid, $guest_conf);
455 PVE
::Tools
::lock_file_full
($STATE_PATH, 30, 0 , $code);
462 my ($conf, $vm_type) = get_guest_config
($vmid);
464 my $storecfg = PVE
::Storage
::config
();
465 my $sync_vol = get_replicatable_volumes
($storecfg, $conf, $vm_type);
468 foreach my $volid (keys %$sync_vol) {
470 PVE
::Storage
::volume_snapshot_list
($storecfg, $volid, 'replica_');
472 if (my $tmp_snap = shift @$list) {
473 $tmp_snap =~ m/^replica_(\d+)$/;
474 die "snapshots are not coherent\n"
475 if defined($time) && !($time eq $1);
483 sub get_last_replica_snap
{
486 my $storecfg = PVE
::Storage
::config
();
487 my $list = PVE
::Storage
::volume_snapshot_list
($storecfg, $volid, 'replica_');
493 my ($vmid, $key, $value) = @_;
495 if ($key eq 'replica_target') {
496 destroy_replica
($vmid);
497 job_enable
($vmid, undef, $value);
502 my $jobs = read_state
();
504 return if !defined($jobs->{$vmid});
506 if ($key eq 'replica_interval') {
507 $jobs->{$vmid}->{interval
} = $value || 15;
508 } elsif ($key eq 'replica_rate_limit'){
509 $jobs->{$vmid}->{limit
} = $value ||
510 delete $jobs->{$vmid}->{limit
};
512 die "Config parameter $key not known";
518 PVE
::Tools
::lock_file_full
($STATE_PATH, 60, 0 , $update);