]> git.proxmox.com Git - pve-guest-common.git/blob - src/PVE/ReplicationConfig.pm
replication config: code cleanup
[pve-guest-common.git] / src / PVE / ReplicationConfig.pm
1 package PVE::ReplicationConfig;
2
3 use strict;
4 use warnings;
5 use Data::Dumper;
6
7 use PVE::Tools;
8 use PVE::JSONSchema qw(get_standard_option);
9 use PVE::INotify;
10 use PVE::SectionConfig;
11 use PVE::CalendarEvent;
12
13 use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
14
15 use base qw(PVE::SectionConfig);
16
17 my $replication_cfg_filename = 'replication.cfg';
18
19 cfs_register_file(
20 $replication_cfg_filename,
21 sub { __PACKAGE__->parse_config(@_); },
22 sub { __PACKAGE__->write_config(@_); },
23 );
24
25 PVE::JSONSchema::register_format('pve-replication-job-id',
26 \&parse_replication_job_id);
27 sub 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));
34 die "$msg (guest IDs < 100 are reserved)\n" if $guest < 100;
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
44 PVE::JSONSchema::register_standard_option('pve-replication-id', {
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>'.",
46 type => 'string', format => 'pve-replication-job-id',
47 pattern => '[1-9][0-9]{2,8}-\d{1,9}',
48 });
49
50 my $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 },
65 remove_job => {
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.",
69 type => 'string',
70 enum => ['local', 'full'],
71 optional => 1,
72 },
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 => {
80 description => "Storage replication schedule. The format is a subset of `systemd` calendar events.",
81 type => 'string', format => 'pve-calendar-event',
82 maxLength => 128,
83 default => '*/15',
84 optional => 1,
85 },
86 source => {
87 description => "For internal use, to detect if the guest was stolen.",
88 type => 'string', format => 'pve-node',
89 optional => 1,
90 },
91 },
92 };
93
94 sub private {
95 return $defaultData;
96 }
97
98 sub parse_section_header {
99 my ($class, $line) = @_;
100
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
104 my $errmsg = undef; # set if you want to skip whole section
105 eval { parse_replication_job_id($id); };
106 $errmsg = $@ if $@;
107 my $config = {};
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.
116 sub get_unique_target_id {
117 my ($class, $data) = @_;
118
119 die "please overwrite in subclass";
120 }
121
122 sub 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
132 my ($guest, $jobnum) = parse_replication_job_id($id);
133
134 $data->{guest} = $guest;
135 $data->{jobnum} = $jobnum;
136 $data->{id} = $id;
137
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
156 sub 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
168 die "property 'guest' has wrong value\n" if $id !~ m/^\Q$vmid\E-/;
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
177 return $class->SUPER::write_config($filename, $cfg);
178 }
179
180 sub 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
190 sub write {
191 my ($cfg) = @_;
192
193 cfs_write_file($replication_cfg_filename, $cfg);
194 }
195
196 sub 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
206 sub 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 }
221
222 sub 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
235 sub 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
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
251 sub 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
264 sub switch_replication_job_target {
265 my ($vmid, $old_target, $new_target) = @_;
266
267 my $update_jobs = sub {
268 my $cfg = PVE::ReplicationConfig->new();
269 $cfg->switch_replication_job_target_nolock($vmid, $old_target, $new_target);
270 };
271 lock($update_jobs);
272 }
273
274 sub 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
286 sub 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
300 package PVE::ReplicationConfig::Cluster;
301
302 use base qw(PVE::ReplicationConfig);
303
304 sub type {
305 return 'local';
306 }
307
308 sub properties {
309 return {
310 target => {
311 description => "Target node.",
312 type => 'string', format => 'pve-node',
313 },
314 };
315 }
316
317 sub options {
318 return {
319 target => { fixed => 1, optional => 0 },
320 disable => { optional => 1 },
321 comment => { optional => 1 },
322 rate => { optional => 1 },
323 schedule => { optional => 1 },
324 remove_job => { optional => 1 },
325 source => { optional => 1 },
326 };
327 }
328
329 sub get_unique_target_id {
330 my ($class, $data) = @_;
331
332 return "local/$data->{target}";
333 }
334
335 PVE::ReplicationConfig::Cluster->register();
336 PVE::ReplicationConfig->init();
337
338 1;