]> git.proxmox.com Git - pve-guest-common.git/blob - PVE/ReplicationConfig.pm
PVE::ReplicationConfig - use new ID format "<vmid>-<num>"
[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_standard_option('pve-replication-id', {
24 description => "Replication Job ID.",
25 type => 'string',
26 pattern => '[1-9][0-9]{2,8}-\d{1,9}',
27 });
28
29 my $defaultData = {
30 propertyList => {
31 type => { description => "Section type." },
32 id => get_standard_option('pve-replication-id'),
33 disable => {
34 description => "Flag to disable/deactivate the entry.",
35 type => 'boolean',
36 optional => 1,
37 },
38 comment => {
39 description => "Description.",
40 type => 'string',
41 optional => 1,
42 maxLength => 4096,
43 },
44 remove_job => {
45 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.",
46 type => 'string',
47 enum => ['local', 'full'],
48 optional => 1,
49 },
50 guest => get_standard_option('pve-vmid', {
51 optional => 1,
52 completion => \&PVE::Cluster::complete_vmid }),
53 rate => {
54 description => "Rate limit in mbps (megabytes per second) as floating point number.",
55 type => 'number',
56 minimum => 1,
57 optional => 1,
58 },
59 schedule => {
60 description => "Storage replication schedule. The format is a subset of `systemd` calender events.",
61 type => 'string', format => 'pve-calendar-event',
62 maxLength => 128,
63 default => '*/15',
64 optional => 1,
65 },
66 },
67 };
68
69 sub private {
70 return $defaultData;
71 }
72
73 sub parse_section_header {
74 my ($class, $line) = @_;
75
76 if ($line =~ m/^(\S+):\s*(\d+)-(\d+)\s*$/) {
77 my ($type, $guest, $subid) = (lc($1), int($2), int($3));
78 my $id = "$guest-$subid"; # use parsed integers
79 my $errmsg = undef; # set if you want to skip whole section
80 eval {
81 die "invalid replication job id '$id'\n" if $subid < 1;
82 PVE::JSONSchema::pve_verify_vmid($guest);
83 };
84 $errmsg = $@ if $@;
85 my $config = { guest => $guest };
86 return ($type, $id, $errmsg, $config);
87 }
88 return undef;
89 }
90
91 # Note: We want only one replication job per target to
92 # avoid confusion. This method should return a string
93 # which uniquely identifies the target.
94 sub get_unique_target_id {
95 my ($class, $data) = @_;
96
97 die "please overwrite in subclass";
98 }
99
100 sub parse_config {
101 my ($class, $filename, $raw) = @_;
102
103 my $cfg = $class->SUPER::parse_config($filename, $raw);
104
105 my $target_hash = {};
106
107 foreach my $id (sort keys %{$cfg->{ids}}) {
108 my $data = $cfg->{ids}->{$id};
109
110 $data->{comment} = PVE::Tools::decode_text($data->{comment})
111 if defined($data->{comment});
112
113 my $plugin = $class->lookup($data->{type});
114 my $tid = $plugin->get_unique_target_id($data);
115 my $vmid = $data->{guest};
116
117 # should not happen, but we want to be sure
118 if (defined($target_hash->{$vmid}->{$tid})) {
119 warn "delete job $id: replication job for guest '$vmid' to target '$tid' already exists\n";
120 delete $cfg->{ids}->{$id};
121 }
122 $target_hash->{$vmid}->{$tid} = 1;
123 }
124
125 return $cfg;
126 }
127
128 sub write_config {
129 my ($class, $filename, $cfg) = @_;
130
131 my $target_hash = {};
132
133 foreach my $id (keys %{$cfg->{ids}}) {
134 my $data = $cfg->{ids}->{$id};
135
136 my $plugin = $class->lookup($data->{type});
137 my $tid = $plugin->get_unique_target_id($data);
138 my $vmid = $data->{guest};
139
140 die "replication job for guest '$vmid' to target '$tid' already exists\n"
141 if defined($target_hash->{$vmid}->{$tid});
142 $target_hash->{$vmid}->{$tid} = 1;
143
144 $data->{comment} = PVE::Tools::encode_text($data->{comment})
145 if defined($data->{comment});
146 }
147
148 $class->SUPER::write_config($filename, $cfg);
149 }
150
151 sub new {
152 my ($type) = @_;
153
154 my $class = ref($type) || $type;
155
156 my $cfg = cfs_read_file($replication_cfg_filename);
157
158 return bless $cfg, $class;
159 }
160
161 sub write {
162 my ($cfg) = @_;
163
164 cfs_write_file($replication_cfg_filename, $cfg);
165 }
166
167 sub lock {
168 my ($code, $errmsg) = @_;
169
170 cfs_lock_file($replication_cfg_filename, undef, $code);
171 my $err = $@;
172 if ($err) {
173 $errmsg ? die "$errmsg: $err" : die $err;
174 }
175 }
176
177 sub check_for_existing_jobs {
178 my ($cfg, $vmid, $noerr) = @_;
179
180 foreach my $id (keys %{$cfg->{ids}}) {
181 my $data = $cfg->{ids}->{$id};
182
183 if ($data->{guest} == $vmid) {
184 return 1 if $noerr;
185 die "There is a replication job '$id' for guest '$vmid' - " .
186 "Please remove that first.\n"
187 }
188 }
189
190 return undef;
191 }
192
193 package PVE::ReplicationConfig::Cluster;
194
195 use base qw(PVE::ReplicationConfig);
196
197 sub type {
198 return 'local';
199 }
200
201 sub properties {
202 return {
203 target => {
204 description => "Target node.",
205 type => 'string', format => 'pve-node',
206 },
207 };
208 }
209
210 sub options {
211 return {
212 guest => { fixed => 1, optional => 0 },
213 target => { fixed => 1, optional => 0 },
214 disable => { optional => 1 },
215 comment => { optional => 1 },
216 rate => { optional => 1 },
217 schedule => { optional => 1 },
218 remove_job => { optional => 1 },
219 };
220 }
221
222 sub get_unique_target_id {
223 my ($class, $data) = @_;
224
225 return "local/$data->{target}";
226 }
227
228 PVE::ReplicationConfig::Cluster->register();
229 PVE::ReplicationConfig->init();
230
231 1;