1 package PVE
::ReplicationConfig
;
8 use PVE
::JSONSchema
qw(get_standard_option);
10 use PVE
::SectionConfig
;
11 use PVE
::CalendarEvent
;
13 use PVE
::Cluster
qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
15 use base
qw(PVE::SectionConfig);
17 my $replication_cfg_filename = 'replication.cfg';
19 cfs_register_file
($replication_cfg_filename,
20 sub { __PACKAGE__-
>parse_config(@_); },
21 sub { __PACKAGE__-
>write_config(@_); });
23 PVE
::JSONSchema
::register_format
('pve-replication-job-id',
24 \
&parse_replication_job_id
);
25 sub parse_replication_job_id
{
26 my ($id, $noerr) = @_;
28 my $msg = "invalid replication job id '$id'";
30 if ($id =~ m/^(\d+)-(\d+)$/) {
31 my ($guest, $jobnum) = (int($1), int($2));
32 die "$msg (guest IDs < 100 are reseved)\n" if $guest < 100;
33 my $parsed_id = "$guest-$jobnum"; # use parsed integers
34 return wantarray ?
($guest, $jobnum, $parsed_id) : $parsed_id;
37 return undef if $noerr;
42 PVE
::JSONSchema
::register_standard_option
('pve-replication-id', {
43 description
=> "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '<GUEST>-<JOBNUM>'.",
44 type
=> 'string', format
=> 'pve-replication-job-id',
45 pattern
=> '[1-9][0-9]{2,8}-\d{1,9}',
50 type
=> { description
=> "Section type." },
51 id
=> get_standard_option
('pve-replication-id'),
53 description
=> "Flag to disable/deactivate the entry.",
58 description
=> "Description.",
64 description
=> "Mark the replication job for removal. The job will remove all local replication snapshots. When set to 'full', it also tries to remove replicated volumes on the target. The job then removes itself from the configuration file.",
66 enum
=> ['local', 'full'],
70 description
=> "Rate limit in mbps (megabytes per second) as floating point number.",
76 description
=> "Storage replication schedule. The format is a subset of `systemd` calender events.",
77 type
=> 'string', format
=> 'pve-calendar-event',
83 description
=> "Source of the replication.",
84 type
=> 'string', format
=> 'pve-node',
94 sub parse_section_header
{
95 my ($class, $line) = @_;
97 if ($line =~ m/^(\S+):\s*(\d+)-(\d+)\s*$/) {
98 my ($type, $guest, $subid) = (lc($1), int($2), int($3));
99 my $id = "$guest-$subid"; # use parsed integers
100 my $errmsg = undef; # set if you want to skip whole section
101 eval { parse_replication_job_id
($id); };
104 return ($type, $id, $errmsg, $config);
109 # Note: We want only one replication job per target to
110 # avoid confusion. This method should return a string
111 # which uniquely identifies the target.
112 sub get_unique_target_id
{
113 my ($class, $data) = @_;
115 die "please overwrite in subclass";
119 my ($class, $filename, $raw) = @_;
121 my $cfg = $class->SUPER::parse_config
($filename, $raw);
123 my $target_hash = {};
125 foreach my $id (sort keys %{$cfg->{ids
}}) {
126 my $data = $cfg->{ids
}->{$id};
128 my ($guest, $jobnum) = parse_replication_job_id
($id);
130 $data->{guest
} = $guest;
131 $data->{jobnum
} = $jobnum;
134 $data->{comment
} = PVE
::Tools
::decode_text
($data->{comment
})
135 if defined($data->{comment
});
137 my $plugin = $class->lookup($data->{type
});
138 my $tid = $plugin->get_unique_target_id($data);
139 my $vmid = $data->{guest
};
141 # should not happen, but we want to be sure
142 if (defined($target_hash->{$vmid}->{$tid})) {
143 warn "delete job $id: replication job for guest '$vmid' to target '$tid' already exists\n";
144 delete $cfg->{ids
}->{$id};
146 $target_hash->{$vmid}->{$tid} = 1;
153 my ($class, $filename, $cfg) = @_;
155 my $target_hash = {};
157 foreach my $id (keys %{$cfg->{ids
}}) {
158 my $data = $cfg->{ids
}->{$id};
160 my $plugin = $class->lookup($data->{type
});
161 my $tid = $plugin->get_unique_target_id($data);
162 my $vmid = $data->{guest
};
164 die "property 'guest' has wrong value\n" if $id !~ m/^\Q$vmid\E-/;
165 die "replication job for guest '$vmid' to target '$tid' already exists\n"
166 if defined($target_hash->{$vmid}->{$tid});
167 $target_hash->{$vmid}->{$tid} = 1;
169 $data->{comment
} = PVE
::Tools
::encode_text
($data->{comment
})
170 if defined($data->{comment
});
173 $class->SUPER::write_config
($filename, $cfg);
179 my $class = ref($type) || $type;
181 my $cfg = cfs_read_file
($replication_cfg_filename);
183 return bless $cfg, $class;
189 cfs_write_file
($replication_cfg_filename, $cfg);
193 my ($code, $errmsg) = @_;
195 cfs_lock_file
($replication_cfg_filename, undef, $code);
198 $errmsg ?
die "$errmsg: $err" : die $err;
202 sub check_for_existing_jobs
{
203 my ($cfg, $vmid, $noerr) = @_;
205 foreach my $id (keys %{$cfg->{ids
}}) {
206 my $data = $cfg->{ids
}->{$id};
208 if ($data->{guest
} == $vmid) {
210 die "There is a replication job '$id' for guest '$vmid' - " .
211 "Please remove that first.\n"
218 sub find_local_replication_job
{
219 my ($cfg, $vmid, $target) = @_;
221 foreach my $id (keys %{$cfg->{ids
}}) {
222 my $data = $cfg->{ids
}->{$id};
224 return $data if $data->{type
} eq 'local' &&
225 $data->{guest
} == $vmid && $data->{target
} eq $target;
231 # switch local replication job target
232 sub switch_replication_job_target
{
233 my ($vmid, $old_target, $new_target) = @_;
235 my $transfer_job = sub {
236 my $cfg = PVE
::ReplicationConfig-
>new();
237 my $jobcfg = find_local_replication_job
($cfg, $vmid, $old_target);
241 $jobcfg->{target
} = $new_target;
253 my $cfg = __PACKAGE__-
>new();
254 delete $cfg->{ids
}->{$jobid};
261 package PVE
::ReplicationConfig
::Cluster
;
263 use base
qw(PVE::ReplicationConfig);
272 description
=> "Target node.",
273 type
=> 'string', format
=> 'pve-node',
280 target
=> { fixed
=> 1, optional
=> 0 },
281 disable
=> { optional
=> 1 },
282 comment
=> { optional
=> 1 },
283 rate
=> { optional
=> 1 },
284 schedule
=> { optional
=> 1 },
285 remove_job
=> { optional
=> 1 },
286 source
=> { optional
=> 1 },
290 sub get_unique_target_id
{
291 my ($class, $data) = @_;
293 return "local/$data->{target}";
296 PVE
::ReplicationConfig
::Cluster-
>register();
297 PVE
::ReplicationConfig-
>init();