]>
Commit | Line | Data |
---|---|---|
87109d74 DM |
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($replication_cfg_filename, | |
20 | sub { __PACKAGE__->parse_config(@_); }, | |
21 | sub { __PACKAGE__->write_config(@_); }); | |
22 | ||
5d31e77b DM |
23 | PVE::JSONSchema::register_format('pve-replication-job-id', |
24 | \&parse_replication_job_id); | |
25 | sub parse_replication_job_id { | |
26 | my ($id, $noerr) = @_; | |
27 | ||
28 | my $msg = "invalid replication job id '$id'"; | |
29 | ||
30 | if ($id =~ m/^(\d+)-(\d+)$/) { | |
31 | my ($guest, $jobnum) = (int($1), int($2)); | |
ab44df53 | 32 | die "$msg (guest IDs < 100 are reserved)\n" if $guest < 100; |
5d31e77b DM |
33 | my $parsed_id = "$guest-$jobnum"; # use parsed integers |
34 | return wantarray ? ($guest, $jobnum, $parsed_id) : $parsed_id; | |
35 | } | |
36 | ||
37 | return undef if $noerr; | |
38 | ||
39 | die "$msg\n"; | |
40 | } | |
41 | ||
87109d74 | 42 | PVE::JSONSchema::register_standard_option('pve-replication-id', { |
0de17556 | 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>'.", |
5d31e77b | 44 | type => 'string', format => 'pve-replication-job-id', |
f87dd81d | 45 | pattern => '[1-9][0-9]{2,8}-\d{1,9}', |
87109d74 DM |
46 | }); |
47 | ||
48 | my $defaultData = { | |
49 | propertyList => { | |
50 | type => { description => "Section type." }, | |
51 | id => get_standard_option('pve-replication-id'), | |
52 | disable => { | |
53 | description => "Flag to disable/deactivate the entry.", | |
54 | type => 'boolean', | |
55 | optional => 1, | |
56 | }, | |
57 | comment => { | |
58 | description => "Description.", | |
59 | type => 'string', | |
60 | optional => 1, | |
61 | maxLength => 4096, | |
62 | }, | |
151f1335 DM |
63 | remove_job => { |
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.", | |
65 | type => 'string', | |
66 | enum => ['local', 'full'], | |
67 | optional => 1, | |
68 | }, | |
87109d74 DM |
69 | rate => { |
70 | description => "Rate limit in mbps (megabytes per second) as floating point number.", | |
71 | type => 'number', | |
72 | minimum => 1, | |
73 | optional => 1, | |
74 | }, | |
75 | schedule => { | |
ab44df53 | 76 | description => "Storage replication schedule. The format is a subset of `systemd` calendar events.", |
87109d74 DM |
77 | type => 'string', format => 'pve-calendar-event', |
78 | maxLength => 128, | |
79 | default => '*/15', | |
80 | optional => 1, | |
81 | }, | |
4ea5167e WL |
82 | source => { |
83 | description => "Source of the replication.", | |
84 | type => 'string', format => 'pve-node', | |
85 | optional => 1, | |
86 | }, | |
87109d74 DM |
87 | }, |
88 | }; | |
89 | ||
90 | sub private { | |
91 | return $defaultData; | |
92 | } | |
93 | ||
94 | sub parse_section_header { | |
95 | my ($class, $line) = @_; | |
96 | ||
f87dd81d DM |
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 | |
87109d74 | 100 | my $errmsg = undef; # set if you want to skip whole section |
5d31e77b | 101 | eval { parse_replication_job_id($id); }; |
87109d74 | 102 | $errmsg = $@ if $@; |
1fcde52a | 103 | my $config = {}; |
87109d74 DM |
104 | return ($type, $id, $errmsg, $config); |
105 | } | |
106 | return undef; | |
107 | } | |
108 | ||
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) = @_; | |
114 | ||
115 | die "please overwrite in subclass"; | |
116 | } | |
117 | ||
118 | sub parse_config { | |
119 | my ($class, $filename, $raw) = @_; | |
120 | ||
121 | my $cfg = $class->SUPER::parse_config($filename, $raw); | |
122 | ||
123 | my $target_hash = {}; | |
124 | ||
125 | foreach my $id (sort keys %{$cfg->{ids}}) { | |
126 | my $data = $cfg->{ids}->{$id}; | |
127 | ||
1fcde52a DM |
128 | my ($guest, $jobnum) = parse_replication_job_id($id); |
129 | ||
130 | $data->{guest} = $guest; | |
131 | $data->{jobnum} = $jobnum; | |
c64fb368 | 132 | $data->{id} = $id; |
1fcde52a | 133 | |
87109d74 DM |
134 | $data->{comment} = PVE::Tools::decode_text($data->{comment}) |
135 | if defined($data->{comment}); | |
136 | ||
137 | my $plugin = $class->lookup($data->{type}); | |
138 | my $tid = $plugin->get_unique_target_id($data); | |
139 | my $vmid = $data->{guest}; | |
140 | ||
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}; | |
145 | } | |
146 | $target_hash->{$vmid}->{$tid} = 1; | |
147 | } | |
148 | ||
149 | return $cfg; | |
150 | } | |
151 | ||
152 | sub write_config { | |
153 | my ($class, $filename, $cfg) = @_; | |
154 | ||
155 | my $target_hash = {}; | |
156 | ||
157 | foreach my $id (keys %{$cfg->{ids}}) { | |
158 | my $data = $cfg->{ids}->{$id}; | |
159 | ||
160 | my $plugin = $class->lookup($data->{type}); | |
161 | my $tid = $plugin->get_unique_target_id($data); | |
162 | my $vmid = $data->{guest}; | |
163 | ||
1fcde52a | 164 | die "property 'guest' has wrong value\n" if $id !~ m/^\Q$vmid\E-/; |
87109d74 DM |
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; | |
168 | ||
169 | $data->{comment} = PVE::Tools::encode_text($data->{comment}) | |
170 | if defined($data->{comment}); | |
171 | } | |
172 | ||
24691c21 | 173 | return $class->SUPER::write_config($filename, $cfg); |
87109d74 DM |
174 | } |
175 | ||
176 | sub new { | |
177 | my ($type) = @_; | |
178 | ||
179 | my $class = ref($type) || $type; | |
180 | ||
181 | my $cfg = cfs_read_file($replication_cfg_filename); | |
182 | ||
183 | return bless $cfg, $class; | |
184 | } | |
185 | ||
186 | sub write { | |
187 | my ($cfg) = @_; | |
188 | ||
189 | cfs_write_file($replication_cfg_filename, $cfg); | |
190 | } | |
191 | ||
192 | sub lock { | |
193 | my ($code, $errmsg) = @_; | |
194 | ||
195 | cfs_lock_file($replication_cfg_filename, undef, $code); | |
196 | my $err = $@; | |
197 | if ($err) { | |
198 | $errmsg ? die "$errmsg: $err" : die $err; | |
199 | } | |
200 | } | |
201 | ||
6e55d55a DM |
202 | sub check_for_existing_jobs { |
203 | my ($cfg, $vmid, $noerr) = @_; | |
204 | ||
205 | foreach my $id (keys %{$cfg->{ids}}) { | |
206 | my $data = $cfg->{ids}->{$id}; | |
207 | ||
208 | if ($data->{guest} == $vmid) { | |
209 | return 1 if $noerr; | |
210 | die "There is a replication job '$id' for guest '$vmid' - " . | |
211 | "Please remove that first.\n" | |
212 | } | |
213 | } | |
214 | ||
215 | return undef; | |
216 | } | |
87109d74 | 217 | |
637b7acd DM |
218 | sub find_local_replication_job { |
219 | my ($cfg, $vmid, $target) = @_; | |
220 | ||
221 | foreach my $id (keys %{$cfg->{ids}}) { | |
222 | my $data = $cfg->{ids}->{$id}; | |
223 | ||
224 | return $data if $data->{type} eq 'local' && | |
225 | $data->{guest} == $vmid && $data->{target} eq $target; | |
226 | } | |
227 | ||
228 | return undef; | |
229 | } | |
230 | ||
18c36925 DM |
231 | # switch local replication job target |
232 | sub switch_replication_job_target { | |
233 | my ($vmid, $old_target, $new_target) = @_; | |
234 | ||
235 | my $transfer_job = sub { | |
236 | my $cfg = PVE::ReplicationConfig->new(); | |
237 | my $jobcfg = find_local_replication_job($cfg, $vmid, $old_target); | |
238 | ||
239 | return if !$jobcfg; | |
240 | ||
241 | $jobcfg->{target} = $new_target; | |
242 | ||
243 | $cfg->write(); | |
244 | }; | |
245 | ||
246 | lock($transfer_job); | |
247 | }; | |
248 | ||
571156ee DM |
249 | sub delete_job { |
250 | my ($jobid) = @_; | |
251 | ||
252 | my $code = sub { | |
253 | my $cfg = __PACKAGE__->new(); | |
254 | delete $cfg->{ids}->{$jobid}; | |
255 | $cfg->write(); | |
256 | }; | |
257 | ||
258 | lock($code); | |
259 | } | |
260 | ||
1daaf2ea CE |
261 | sub remove_vmid_jobs { |
262 | my ($vmid) = @_; | |
263 | ||
264 | my $code = sub { | |
265 | my $cfg = __PACKAGE__->new(); | |
266 | foreach my $id (keys %{$cfg->{ids}}) { | |
267 | delete $cfg->{ids}->{$id} if ($cfg->{ids}->{$id}->{guest} == $vmid); | |
268 | } | |
269 | $cfg->write(); | |
270 | }; | |
271 | ||
272 | lock($code); | |
273 | } | |
274 | ||
286a9ab9 WL |
275 | sub swap_source_target_nolock { |
276 | my ($jobid) = @_; | |
277 | ||
278 | my $cfg = __PACKAGE__->new(); | |
279 | my $job = $cfg->{ids}->{$jobid}; | |
280 | my $tmp = $job->{source}; | |
281 | $job->{source} = $job->{target}; | |
282 | $job->{target} = $tmp; | |
283 | $cfg->write(); | |
284 | ||
285 | return $cfg->{ids}->{$jobid}; | |
286 | } | |
287 | ||
87109d74 DM |
288 | package PVE::ReplicationConfig::Cluster; |
289 | ||
290 | use base qw(PVE::ReplicationConfig); | |
291 | ||
292 | sub type { | |
293 | return 'local'; | |
294 | } | |
295 | ||
296 | sub properties { | |
297 | return { | |
298 | target => { | |
299 | description => "Target node.", | |
300 | type => 'string', format => 'pve-node', | |
301 | }, | |
302 | }; | |
303 | } | |
304 | ||
305 | sub options { | |
306 | return { | |
87109d74 DM |
307 | target => { fixed => 1, optional => 0 }, |
308 | disable => { optional => 1 }, | |
309 | comment => { optional => 1 }, | |
310 | rate => { optional => 1 }, | |
311 | schedule => { optional => 1 }, | |
151f1335 | 312 | remove_job => { optional => 1 }, |
4ea5167e | 313 | source => { optional => 1 }, |
87109d74 DM |
314 | }; |
315 | } | |
316 | ||
317 | sub get_unique_target_id { | |
318 | my ($class, $data) = @_; | |
319 | ||
320 | return "local/$data->{target}"; | |
321 | } | |
322 | ||
323 | PVE::ReplicationConfig::Cluster->register(); | |
324 | PVE::ReplicationConfig->init(); | |
325 | ||
326 | 1; |