]> git.proxmox.com Git - pve-guest-common.git/blame - PVE/ReplicationConfig.pm
cleanup: ReplicationConfig: be specific about write_config
[pve-guest-common.git] / 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
19cfs_register_file($replication_cfg_filename,
20 sub { __PACKAGE__->parse_config(@_); },
21 sub { __PACKAGE__->write_config(@_); });
22
5d31e77b
DM
23PVE::JSONSchema::register_format('pve-replication-job-id',
24 \&parse_replication_job_id);
25sub 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
87109d74 42PVE::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
48my $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 => {
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 },
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
90sub private {
91 return $defaultData;
92}
93
94sub 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.
112sub get_unique_target_id {
113 my ($class, $data) = @_;
114
115 die "please overwrite in subclass";
116}
117
118sub 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
152sub 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
176sub 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
186sub write {
187 my ($cfg) = @_;
188
189 cfs_write_file($replication_cfg_filename, $cfg);
190}
191
192sub 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
202sub 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
218sub 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
232sub 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
249sub 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
286a9ab9
WL
261sub 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
87109d74
DM
274package PVE::ReplicationConfig::Cluster;
275
276use base qw(PVE::ReplicationConfig);
277
278sub type {
279 return 'local';
280}
281
282sub properties {
283 return {
284 target => {
285 description => "Target node.",
286 type => 'string', format => 'pve-node',
287 },
288 };
289}
290
291sub options {
292 return {
87109d74
DM
293 target => { fixed => 1, optional => 0 },
294 disable => { optional => 1 },
295 comment => { optional => 1 },
296 rate => { optional => 1 },
297 schedule => { optional => 1 },
151f1335 298 remove_job => { optional => 1 },
4ea5167e 299 source => { optional => 1 },
87109d74
DM
300 };
301}
302
303sub get_unique_target_id {
304 my ($class, $data) = @_;
305
306 return "local/$data->{target}";
307}
308
309PVE::ReplicationConfig::Cluster->register();
310PVE::ReplicationConfig->init();
311
3121;