]> git.proxmox.com Git - pve-guest-common.git/blob - PVE/ReplicationConfig.pm
Add guest type at find_local_replication_job
[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 },
83 };
84
85 sub private {
86 return $defaultData;
87 }
88
89 sub parse_section_header {
90 my ($class, $line) = @_;
91
92 if ($line =~ m/^(\S+):\s*(\d+)-(\d+)\s*$/) {
93 my ($type, $guest, $subid) = (lc($1), int($2), int($3));
94 my $id = "$guest-$subid"; # use parsed integers
95 my $errmsg = undef; # set if you want to skip whole section
96 eval { parse_replication_job_id($id); };
97 $errmsg = $@ if $@;
98 my $config = {};
99 return ($type, $id, $errmsg, $config);
100 }
101 return undef;
102 }
103
104 # Note: We want only one replication job per target to
105 # avoid confusion. This method should return a string
106 # which uniquely identifies the target.
107 sub get_unique_target_id {
108 my ($class, $data) = @_;
109
110 die "please overwrite in subclass";
111 }
112
113 sub parse_config {
114 my ($class, $filename, $raw) = @_;
115
116 my $cfg = $class->SUPER::parse_config($filename, $raw);
117
118 my $target_hash = {};
119
120 foreach my $id (sort keys %{$cfg->{ids}}) {
121 my $data = $cfg->{ids}->{$id};
122
123 my ($guest, $jobnum) = parse_replication_job_id($id);
124
125 $data->{guest} = $guest;
126 $data->{jobnum} = $jobnum;
127 $data->{id} = $id;
128
129 $data->{comment} = PVE::Tools::decode_text($data->{comment})
130 if defined($data->{comment});
131
132 my $plugin = $class->lookup($data->{type});
133 my $tid = $plugin->get_unique_target_id($data);
134 my $vmid = $data->{guest};
135
136 # should not happen, but we want to be sure
137 if (defined($target_hash->{$vmid}->{$tid})) {
138 warn "delete job $id: replication job for guest '$vmid' to target '$tid' already exists\n";
139 delete $cfg->{ids}->{$id};
140 }
141 $target_hash->{$vmid}->{$tid} = 1;
142 }
143
144 return $cfg;
145 }
146
147 sub write_config {
148 my ($class, $filename, $cfg) = @_;
149
150 my $target_hash = {};
151
152 foreach my $id (keys %{$cfg->{ids}}) {
153 my $data = $cfg->{ids}->{$id};
154
155 my $plugin = $class->lookup($data->{type});
156 my $tid = $plugin->get_unique_target_id($data);
157 my $vmid = $data->{guest};
158
159 die "property 'guest' has wrong value\n" if $id !~ m/^\Q$vmid\E-/;
160 die "replication job for guest '$vmid' to target '$tid' already exists\n"
161 if defined($target_hash->{$vmid}->{$tid});
162 $target_hash->{$vmid}->{$tid} = 1;
163
164 $data->{comment} = PVE::Tools::encode_text($data->{comment})
165 if defined($data->{comment});
166 }
167
168 $class->SUPER::write_config($filename, $cfg);
169 }
170
171 sub new {
172 my ($type) = @_;
173
174 my $class = ref($type) || $type;
175
176 my $cfg = cfs_read_file($replication_cfg_filename);
177
178 return bless $cfg, $class;
179 }
180
181 sub write {
182 my ($cfg) = @_;
183
184 cfs_write_file($replication_cfg_filename, $cfg);
185 }
186
187 sub lock {
188 my ($code, $errmsg) = @_;
189
190 cfs_lock_file($replication_cfg_filename, undef, $code);
191 my $err = $@;
192 if ($err) {
193 $errmsg ? die "$errmsg: $err" : die $err;
194 }
195 }
196
197 sub check_for_existing_jobs {
198 my ($cfg, $vmid, $noerr) = @_;
199
200 foreach my $id (keys %{$cfg->{ids}}) {
201 my $data = $cfg->{ids}->{$id};
202
203 if ($data->{guest} == $vmid) {
204 return 1 if $noerr;
205 die "There is a replication job '$id' for guest '$vmid' - " .
206 "Please remove that first.\n"
207 }
208 }
209
210 return undef;
211 }
212
213 sub find_local_replication_job {
214 my ($cfg, $vmid, $target) = @_;
215
216 my $vms = PVE::Cluster::get_vmlist();
217
218 foreach my $id (keys %{$cfg->{ids}}) {
219 my $data = $cfg->{ids}->{$id};
220
221 $data->{vmtype} = $vms->{ids}->{$vmid}->{type};
222
223 return $data if $data->{type} eq 'local' &&
224 $data->{guest} == $vmid && $data->{target} eq $target;
225 }
226
227 return undef;
228 }
229
230 # switch local replication job target
231 sub switch_replication_job_target {
232 my ($vmid, $old_target, $new_target) = @_;
233
234 my $transfer_job = sub {
235 my $cfg = PVE::ReplicationConfig->new();
236 my $jobcfg = find_local_replication_job($cfg, $vmid, $old_target);
237
238 return if !$jobcfg;
239
240 $jobcfg->{target} = $new_target;
241
242 $cfg->write();
243 };
244
245 lock($transfer_job);
246 };
247
248 sub delete_job {
249 my ($jobid) = @_;
250
251 my $code = sub {
252 my $cfg = __PACKAGE__->new();
253 delete $cfg->{ids}->{$jobid};
254 $cfg->write();
255 };
256
257 lock($code);
258 }
259
260 package PVE::ReplicationConfig::Cluster;
261
262 use base qw(PVE::ReplicationConfig);
263
264 sub type {
265 return 'local';
266 }
267
268 sub properties {
269 return {
270 target => {
271 description => "Target node.",
272 type => 'string', format => 'pve-node',
273 },
274 };
275 }
276
277 sub options {
278 return {
279 target => { fixed => 1, optional => 0 },
280 disable => { optional => 1 },
281 comment => { optional => 1 },
282 rate => { optional => 1 },
283 schedule => { optional => 1 },
284 remove_job => { optional => 1 },
285 };
286 }
287
288 sub get_unique_target_id {
289 my ($class, $data) = @_;
290
291 return "local/$data->{target}";
292 }
293
294 PVE::ReplicationConfig::Cluster->register();
295 PVE::ReplicationConfig->init();
296
297 1;