]> git.proxmox.com Git - pve-guest-common.git/blob - PVE/ReplicationConfig.pm
cleanup: ReplicationConfig: be specific about write_config
[pve-guest-common.git] / 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($replication_cfg_filename,
20 sub { __PACKAGE__->parse_config(@_); },
21 sub { __PACKAGE__->write_config(@_); });
22
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));
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;
35 }
36
37 return undef if $noerr;
38
39 die "$msg\n";
40 }
41
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}',
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 },
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 },
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 => {
76 description => "Storage replication schedule. The format is a subset of `systemd` calender events.",
77 type => 'string', format => 'pve-calendar-event',
78 maxLength => 128,
79 default => '*/15',
80 optional => 1,
81 },
82 source => {
83 description => "Source of the replication.",
84 type => 'string', format => 'pve-node',
85 optional => 1,
86 },
87 },
88 };
89
90 sub private {
91 return $defaultData;
92 }
93
94 sub parse_section_header {
95 my ($class, $line) = @_;
96
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); };
102 $errmsg = $@ if $@;
103 my $config = {};
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
128 my ($guest, $jobnum) = parse_replication_job_id($id);
129
130 $data->{guest} = $guest;
131 $data->{jobnum} = $jobnum;
132 $data->{id} = $id;
133
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
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;
168
169 $data->{comment} = PVE::Tools::encode_text($data->{comment})
170 if defined($data->{comment});
171 }
172
173 return $class->SUPER::write_config($filename, $cfg);
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
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 }
217
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
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
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
261 sub swap_source_target_nolock {
262 my ($jobid) = @_;
263
264 my $cfg = __PACKAGE__->new();
265 my $job = $cfg->{ids}->{$jobid};
266 my $tmp = $job->{source};
267 $job->{source} = $job->{target};
268 $job->{target} = $tmp;
269 $cfg->write();
270
271 return $cfg->{ids}->{$jobid};
272 }
273
274 package PVE::ReplicationConfig::Cluster;
275
276 use base qw(PVE::ReplicationConfig);
277
278 sub type {
279 return 'local';
280 }
281
282 sub properties {
283 return {
284 target => {
285 description => "Target node.",
286 type => 'string', format => 'pve-node',
287 },
288 };
289 }
290
291 sub options {
292 return {
293 target => { fixed => 1, optional => 0 },
294 disable => { optional => 1 },
295 comment => { optional => 1 },
296 rate => { optional => 1 },
297 schedule => { optional => 1 },
298 remove_job => { optional => 1 },
299 source => { optional => 1 },
300 };
301 }
302
303 sub get_unique_target_id {
304 my ($class, $data) = @_;
305
306 return "local/$data->{target}";
307 }
308
309 PVE::ReplicationConfig::Cluster->register();
310 PVE::ReplicationConfig->init();
311
312 1;