sub { __PACKAGE__->parse_config(@_); },
sub { __PACKAGE__->write_config(@_); });
+PVE::JSONSchema::register_format('pve-replication-job-id',
+ \&parse_replication_job_id);
+sub parse_replication_job_id {
+ my ($id, $noerr) = @_;
+
+ my $msg = "invalid replication job id '$id'";
+
+ if ($id =~ m/^(\d+)-(\d+)$/) {
+ my ($guest, $jobnum) = (int($1), int($2));
+ die "$msg (guest IDs < 100 are reserved)\n" if $guest < 100;
+ my $parsed_id = "$guest-$jobnum"; # use parsed integers
+ return wantarray ? ($guest, $jobnum, $parsed_id) : $parsed_id;
+ }
+
+ return undef if $noerr;
+
+ die "$msg\n";
+}
+
PVE::JSONSchema::register_standard_option('pve-replication-id', {
- description => "Replication Job ID.",
- type => 'string', format => 'pve-configid',
- maxLength => 32, # keep short to reduce snapshot name length
+ description => "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '<GUEST>-<JOBNUM>'.",
+ type => 'string', format => 'pve-replication-job-id',
+ pattern => '[1-9][0-9]{2,8}-\d{1,9}',
});
my $defaultData = {
enum => ['local', 'full'],
optional => 1,
},
- guest => get_standard_option('pve-vmid', {
- optional => 1,
- completion => \&PVE::Cluster::complete_vmid }),
rate => {
description => "Rate limit in mbps (megabytes per second) as floating point number.",
type => 'number',
optional => 1,
},
schedule => {
- description => "Storage replication schedule. The format is a subset of `systemd` calender events.",
+ description => "Storage replication schedule. The format is a subset of `systemd` calendar events.",
type => 'string', format => 'pve-calendar-event',
maxLength => 128,
default => '*/15',
optional => 1,
},
+ source => {
+ description => "Source of the replication.",
+ type => 'string', format => 'pve-node',
+ optional => 1,
+ },
},
};
sub parse_section_header {
my ($class, $line) = @_;
- if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
- my ($type, $id) = (lc($1), $2);
+ if ($line =~ m/^(\S+):\s*(\d+)-(\d+)\s*$/) {
+ my ($type, $guest, $subid) = (lc($1), int($2), int($3));
+ my $id = "$guest-$subid"; # use parsed integers
my $errmsg = undef; # set if you want to skip whole section
- eval { PVE::JSONSchema::pve_verify_configid($id); };
+ eval { parse_replication_job_id($id); };
$errmsg = $@ if $@;
my $config = {};
return ($type, $id, $errmsg, $config);
foreach my $id (sort keys %{$cfg->{ids}}) {
my $data = $cfg->{ids}->{$id};
+ my ($guest, $jobnum) = parse_replication_job_id($id);
+
+ $data->{guest} = $guest;
+ $data->{jobnum} = $jobnum;
+ $data->{id} = $id;
+
$data->{comment} = PVE::Tools::decode_text($data->{comment})
if defined($data->{comment});
my $tid = $plugin->get_unique_target_id($data);
my $vmid = $data->{guest};
+ die "property 'guest' has wrong value\n" if $id !~ m/^\Q$vmid\E-/;
die "replication job for guest '$vmid' to target '$tid' already exists\n"
if defined($target_hash->{$vmid}->{$tid});
$target_hash->{$vmid}->{$tid} = 1;
if defined($data->{comment});
}
- $class->SUPER::write_config($filename, $cfg);
+ return $class->SUPER::write_config($filename, $cfg);
}
sub new {
return undef;
}
+sub find_local_replication_job {
+ my ($cfg, $vmid, $target) = @_;
+
+ foreach my $id (keys %{$cfg->{ids}}) {
+ my $data = $cfg->{ids}->{$id};
+
+ return $data if $data->{type} eq 'local' &&
+ $data->{guest} == $vmid && $data->{target} eq $target;
+ }
+
+ return undef;
+}
+
+# switch local replication job target
+sub switch_replication_job_target {
+ my ($vmid, $old_target, $new_target) = @_;
+
+ my $transfer_job = sub {
+ my $cfg = PVE::ReplicationConfig->new();
+ my $jobcfg = find_local_replication_job($cfg, $vmid, $old_target);
+
+ return if !$jobcfg;
+
+ $jobcfg->{target} = $new_target;
+
+ $cfg->write();
+ };
+
+ lock($transfer_job);
+};
+
+sub delete_job {
+ my ($jobid) = @_;
+
+ my $code = sub {
+ my $cfg = __PACKAGE__->new();
+ delete $cfg->{ids}->{$jobid};
+ $cfg->write();
+ };
+
+ lock($code);
+}
+
+sub remove_vmid_jobs {
+ my ($vmid) = @_;
+
+ my $code = sub {
+ my $cfg = __PACKAGE__->new();
+ foreach my $id (keys %{$cfg->{ids}}) {
+ delete $cfg->{ids}->{$id} if ($cfg->{ids}->{$id}->{guest} == $vmid);
+ }
+ $cfg->write();
+ };
+
+ lock($code);
+}
+
+sub swap_source_target_nolock {
+ my ($jobid) = @_;
+
+ my $cfg = __PACKAGE__->new();
+ my $job = $cfg->{ids}->{$jobid};
+ my $tmp = $job->{source};
+ $job->{source} = $job->{target};
+ $job->{target} = $tmp;
+ $cfg->write();
+
+ return $cfg->{ids}->{$jobid};
+}
+
package PVE::ReplicationConfig::Cluster;
use base qw(PVE::ReplicationConfig);
sub options {
return {
- guest => { fixed => 1, optional => 0 },
target => { fixed => 1, optional => 0 },
disable => { optional => 1 },
comment => { optional => 1 },
rate => { optional => 1 },
schedule => { optional => 1 },
remove_job => { optional => 1 },
+ source => { optional => 1 },
};
}