]> git.proxmox.com Git - pve-guest-common.git/blame - src/PVE/ReplicationConfig.pm
replication config: code cleanup
[pve-guest-common.git] / src / PVE / ReplicationConfig.pm
CommitLineData
87109d74
DM
1package PVE::ReplicationConfig;
2
3use strict;
4use warnings;
5use Data::Dumper;
6
7use PVE::Tools;
8use PVE::JSONSchema qw(get_standard_option);
9use PVE::INotify;
10use PVE::SectionConfig;
11use PVE::CalendarEvent;
12
13use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
14
15use base qw(PVE::SectionConfig);
16
17my $replication_cfg_filename = 'replication.cfg';
18
3bf8e49a
TL
19cfs_register_file(
20 $replication_cfg_filename,
21 sub { __PACKAGE__->parse_config(@_); },
22 sub { __PACKAGE__->write_config(@_); },
23);
87109d74 24
5d31e77b
DM
25PVE::JSONSchema::register_format('pve-replication-job-id',
26 \&parse_replication_job_id);
27sub parse_replication_job_id {
28 my ($id, $noerr) = @_;
29
30 my $msg = "invalid replication job id '$id'";
31
32 if ($id =~ m/^(\d+)-(\d+)$/) {
33 my ($guest, $jobnum) = (int($1), int($2));
ab44df53 34 die "$msg (guest IDs < 100 are reserved)\n" if $guest < 100;
5d31e77b
DM
35 my $parsed_id = "$guest-$jobnum"; # use parsed integers
36 return wantarray ? ($guest, $jobnum, $parsed_id) : $parsed_id;
37 }
38
39 return undef if $noerr;
40
41 die "$msg\n";
42}
43
87109d74 44PVE::JSONSchema::register_standard_option('pve-replication-id', {
0de17556 45 description => "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '<GUEST>-<JOBNUM>'.",
5d31e77b 46 type => 'string', format => 'pve-replication-job-id',
f87dd81d 47 pattern => '[1-9][0-9]{2,8}-\d{1,9}',
87109d74
DM
48});
49
50my $defaultData = {
51 propertyList => {
52 type => { description => "Section type." },
53 id => get_standard_option('pve-replication-id'),
54 disable => {
55 description => "Flag to disable/deactivate the entry.",
56 type => 'boolean',
57 optional => 1,
58 },
59 comment => {
60 description => "Description.",
61 type => 'string',
62 optional => 1,
63 maxLength => 4096,
64 },
151f1335 65 remove_job => {
3bf8e49a
TL
66 description => "Mark the replication job for removal. The job will remove all local"
67 ." replication snapshots. When set to 'full', it also tries to remove replicated"
68 ." volumes on the target. The job then removes itself from the configuration file.",
151f1335
DM
69 type => 'string',
70 enum => ['local', 'full'],
71 optional => 1,
72 },
87109d74
DM
73 rate => {
74 description => "Rate limit in mbps (megabytes per second) as floating point number.",
75 type => 'number',
76 minimum => 1,
77 optional => 1,
78 },
79 schedule => {
ab44df53 80 description => "Storage replication schedule. The format is a subset of `systemd` calendar events.",
87109d74
DM
81 type => 'string', format => 'pve-calendar-event',
82 maxLength => 128,
83 default => '*/15',
84 optional => 1,
85 },
4ea5167e 86 source => {
6364fd63 87 description => "For internal use, to detect if the guest was stolen.",
4ea5167e
WL
88 type => 'string', format => 'pve-node',
89 optional => 1,
90 },
87109d74
DM
91 },
92};
93
94sub private {
95 return $defaultData;
96}
97
98sub parse_section_header {
99 my ($class, $line) = @_;
100
f87dd81d
DM
101 if ($line =~ m/^(\S+):\s*(\d+)-(\d+)\s*$/) {
102 my ($type, $guest, $subid) = (lc($1), int($2), int($3));
103 my $id = "$guest-$subid"; # use parsed integers
87109d74 104 my $errmsg = undef; # set if you want to skip whole section
5d31e77b 105 eval { parse_replication_job_id($id); };
87109d74 106 $errmsg = $@ if $@;
1fcde52a 107 my $config = {};
87109d74
DM
108 return ($type, $id, $errmsg, $config);
109 }
110 return undef;
111}
112
113# Note: We want only one replication job per target to
114# avoid confusion. This method should return a string
115# which uniquely identifies the target.
116sub get_unique_target_id {
117 my ($class, $data) = @_;
118
119 die "please overwrite in subclass";
120}
121
122sub parse_config {
123 my ($class, $filename, $raw) = @_;
124
125 my $cfg = $class->SUPER::parse_config($filename, $raw);
126
127 my $target_hash = {};
128
129 foreach my $id (sort keys %{$cfg->{ids}}) {
130 my $data = $cfg->{ids}->{$id};
131
1fcde52a
DM
132 my ($guest, $jobnum) = parse_replication_job_id($id);
133
134 $data->{guest} = $guest;
135 $data->{jobnum} = $jobnum;
c64fb368 136 $data->{id} = $id;
1fcde52a 137
87109d74
DM
138 $data->{comment} = PVE::Tools::decode_text($data->{comment})
139 if defined($data->{comment});
140
141 my $plugin = $class->lookup($data->{type});
142 my $tid = $plugin->get_unique_target_id($data);
143 my $vmid = $data->{guest};
144
145 # should not happen, but we want to be sure
146 if (defined($target_hash->{$vmid}->{$tid})) {
147 warn "delete job $id: replication job for guest '$vmid' to target '$tid' already exists\n";
148 delete $cfg->{ids}->{$id};
149 }
150 $target_hash->{$vmid}->{$tid} = 1;
151 }
152
153 return $cfg;
154}
155
156sub write_config {
157 my ($class, $filename, $cfg) = @_;
158
159 my $target_hash = {};
160
161 foreach my $id (keys %{$cfg->{ids}}) {
162 my $data = $cfg->{ids}->{$id};
163
164 my $plugin = $class->lookup($data->{type});
165 my $tid = $plugin->get_unique_target_id($data);
166 my $vmid = $data->{guest};
167
1fcde52a 168 die "property 'guest' has wrong value\n" if $id !~ m/^\Q$vmid\E-/;
87109d74
DM
169 die "replication job for guest '$vmid' to target '$tid' already exists\n"
170 if defined($target_hash->{$vmid}->{$tid});
171 $target_hash->{$vmid}->{$tid} = 1;
172
173 $data->{comment} = PVE::Tools::encode_text($data->{comment})
174 if defined($data->{comment});
175 }
176
24691c21 177 return $class->SUPER::write_config($filename, $cfg);
87109d74
DM
178}
179
180sub new {
181 my ($type) = @_;
182
183 my $class = ref($type) || $type;
184
185 my $cfg = cfs_read_file($replication_cfg_filename);
186
187 return bless $cfg, $class;
188}
189
190sub write {
191 my ($cfg) = @_;
192
193 cfs_write_file($replication_cfg_filename, $cfg);
194}
195
196sub lock {
197 my ($code, $errmsg) = @_;
198
199 cfs_lock_file($replication_cfg_filename, undef, $code);
200 my $err = $@;
201 if ($err) {
202 $errmsg ? die "$errmsg: $err" : die $err;
203 }
204}
205
6e55d55a
DM
206sub check_for_existing_jobs {
207 my ($cfg, $vmid, $noerr) = @_;
208
209 foreach my $id (keys %{$cfg->{ids}}) {
210 my $data = $cfg->{ids}->{$id};
211
212 if ($data->{guest} == $vmid) {
213 return 1 if $noerr;
214 die "There is a replication job '$id' for guest '$vmid' - " .
215 "Please remove that first.\n"
216 }
217 }
218
219 return undef;
220}
87109d74 221
637b7acd
DM
222sub find_local_replication_job {
223 my ($cfg, $vmid, $target) = @_;
224
225 foreach my $id (keys %{$cfg->{ids}}) {
226 my $data = $cfg->{ids}->{$id};
227
228 return $data if $data->{type} eq 'local' &&
229 $data->{guest} == $vmid && $data->{target} eq $target;
230 }
231
232 return undef;
233}
234
602ca77c
FE
235sub list_guests_local_replication_jobs {
236 my ($cfg, $vmid) = @_;
237
238 my $jobs = [];
239
240 for my $job (values %{$cfg->{ids}}) {
241 next if $job->{type} ne 'local' || $job->{guest} != $vmid;
242
243 push @{$jobs}, $job;
244 }
245
246 return $jobs;
247}
248
158c90bf
FE
249# makes old_target the new source for all local jobs of this guest
250# makes new_target the target for the single local job with target old_target
0c3550c0
FE
251sub switch_replication_job_target_nolock {
252 my ($cfg, $vmid, $old_target, $new_target) = @_;
253
254 foreach my $jobcfg (values %{$cfg->{ids}}) {
255 next if $jobcfg->{guest} ne $vmid;
256 next if $jobcfg->{type} ne 'local';
257
258 $jobcfg->{target} = $new_target if $jobcfg->{target} eq $old_target;
259 $jobcfg->{source} = $old_target;
260 }
261 $cfg->write();
262}
263
18c36925
DM
264sub switch_replication_job_target {
265 my ($vmid, $old_target, $new_target) = @_;
266
158c90bf 267 my $update_jobs = sub {
18c36925 268 my $cfg = PVE::ReplicationConfig->new();
0c3550c0 269 $cfg->switch_replication_job_target_nolock($vmid, $old_target, $new_target);
18c36925 270 };
158c90bf
FE
271 lock($update_jobs);
272}
18c36925 273
571156ee
DM
274sub delete_job {
275 my ($jobid) = @_;
276
277 my $code = sub {
278 my $cfg = __PACKAGE__->new();
279 delete $cfg->{ids}->{$jobid};
280 $cfg->write();
281 };
282
283 lock($code);
284}
285
1daaf2ea
CE
286sub remove_vmid_jobs {
287 my ($vmid) = @_;
288
289 my $code = sub {
290 my $cfg = __PACKAGE__->new();
291 foreach my $id (keys %{$cfg->{ids}}) {
292 delete $cfg->{ids}->{$id} if ($cfg->{ids}->{$id}->{guest} == $vmid);
293 }
294 $cfg->write();
295 };
296
297 lock($code);
298}
299
87109d74
DM
300package PVE::ReplicationConfig::Cluster;
301
302use base qw(PVE::ReplicationConfig);
303
304sub type {
305 return 'local';
306}
307
308sub properties {
309 return {
310 target => {
311 description => "Target node.",
312 type => 'string', format => 'pve-node',
313 },
314 };
315}
316
317sub options {
318 return {
87109d74
DM
319 target => { fixed => 1, optional => 0 },
320 disable => { optional => 1 },
321 comment => { optional => 1 },
322 rate => { optional => 1 },
323 schedule => { optional => 1 },
151f1335 324 remove_job => { optional => 1 },
4ea5167e 325 source => { optional => 1 },
87109d74
DM
326 };
327}
328
329sub get_unique_target_id {
330 my ($class, $data) = @_;
331
332 return "local/$data->{target}";
333}
334
335PVE::ReplicationConfig::Cluster->register();
336PVE::ReplicationConfig->init();
337
3381;