]> git.proxmox.com Git - pve-guest-common.git/blob - PVE/ReplicationConfig.pm
PVE::ReplicationConfig add delete_job helper
[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
128 $data->{comment} = PVE::Tools::decode_text($data->{comment})
129 if defined($data->{comment});
130
131 my $plugin = $class->lookup($data->{type});
132 my $tid = $plugin->get_unique_target_id($data);
133 my $vmid = $data->{guest};
134
135 # should not happen, but we want to be sure
136 if (defined($target_hash->{$vmid}->{$tid})) {
137 warn "delete job $id: replication job for guest '$vmid' to target '$tid' already exists\n";
138 delete $cfg->{ids}->{$id};
139 }
140 $target_hash->{$vmid}->{$tid} = 1;
141 }
142
143 return $cfg;
144 }
145
146 sub write_config {
147 my ($class, $filename, $cfg) = @_;
148
149 my $target_hash = {};
150
151 foreach my $id (keys %{$cfg->{ids}}) {
152 my $data = $cfg->{ids}->{$id};
153
154 my $plugin = $class->lookup($data->{type});
155 my $tid = $plugin->get_unique_target_id($data);
156 my $vmid = $data->{guest};
157
158 die "property 'guest' has wrong value\n" if $id !~ m/^\Q$vmid\E-/;
159 die "replication job for guest '$vmid' to target '$tid' already exists\n"
160 if defined($target_hash->{$vmid}->{$tid});
161 $target_hash->{$vmid}->{$tid} = 1;
162
163 $data->{comment} = PVE::Tools::encode_text($data->{comment})
164 if defined($data->{comment});
165 }
166
167 $class->SUPER::write_config($filename, $cfg);
168 }
169
170 sub new {
171 my ($type) = @_;
172
173 my $class = ref($type) || $type;
174
175 my $cfg = cfs_read_file($replication_cfg_filename);
176
177 return bless $cfg, $class;
178 }
179
180 sub write {
181 my ($cfg) = @_;
182
183 cfs_write_file($replication_cfg_filename, $cfg);
184 }
185
186 sub lock {
187 my ($code, $errmsg) = @_;
188
189 cfs_lock_file($replication_cfg_filename, undef, $code);
190 my $err = $@;
191 if ($err) {
192 $errmsg ? die "$errmsg: $err" : die $err;
193 }
194 }
195
196 sub check_for_existing_jobs {
197 my ($cfg, $vmid, $noerr) = @_;
198
199 foreach my $id (keys %{$cfg->{ids}}) {
200 my $data = $cfg->{ids}->{$id};
201
202 if ($data->{guest} == $vmid) {
203 return 1 if $noerr;
204 die "There is a replication job '$id' for guest '$vmid' - " .
205 "Please remove that first.\n"
206 }
207 }
208
209 return undef;
210 }
211
212 sub delete_job {
213 my ($jobid) = @_;
214
215 my $code = sub {
216 my $cfg = __PACKAGE__->new();
217 delete $cfg->{ids}->{$jobid};
218 $cfg->write();
219 };
220
221 lock($code);
222 }
223
224 package PVE::ReplicationConfig::Cluster;
225
226 use base qw(PVE::ReplicationConfig);
227
228 sub type {
229 return 'local';
230 }
231
232 sub properties {
233 return {
234 target => {
235 description => "Target node.",
236 type => 'string', format => 'pve-node',
237 },
238 };
239 }
240
241 sub options {
242 return {
243 target => { fixed => 1, optional => 0 },
244 disable => { optional => 1 },
245 comment => { optional => 1 },
246 rate => { optional => 1 },
247 schedule => { optional => 1 },
248 remove_job => { optional => 1 },
249 };
250 }
251
252 sub get_unique_target_id {
253 my ($class, $data) = @_;
254
255 return "local/$data->{target}";
256 }
257
258 PVE::ReplicationConfig::Cluster->register();
259 PVE::ReplicationConfig->init();
260
261 1;