1 package PVE
::ReplicationConfig
;
8 use PVE
::JSONSchema
qw(get_standard_option);
10 use PVE
::SectionConfig
;
11 use PVE
::CalendarEvent
;
13 use PVE
::Cluster
qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file);
15 use base
qw(PVE::SectionConfig);
17 my $replication_cfg_filename = 'replication.cfg';
20 $replication_cfg_filename,
21 sub { __PACKAGE__-
>parse_config(@_); },
22 sub { __PACKAGE__-
>write_config(@_); },
25 PVE
::JSONSchema
::register_format
('pve-replication-job-id',
26 \
&parse_replication_job_id
);
27 sub parse_replication_job_id
{
28 my ($id, $noerr) = @_;
30 my $msg = "invalid replication job id '$id'";
32 if ($id =~ m/^(\d+)-(\d+)$/) {
33 my ($guest, $jobnum) = (int($1), int($2));
34 die "$msg (guest IDs < 100 are reserved)\n" if $guest < 100;
35 my $parsed_id = "$guest-$jobnum"; # use parsed integers
36 return wantarray ?
($guest, $jobnum, $parsed_id) : $parsed_id;
39 return undef if $noerr;
44 PVE
::JSONSchema
::register_standard_option
('pve-replication-id', {
45 description
=> "Replication Job ID. The ID is composed of a Guest ID and a job number, separated by a hyphen, i.e. '<GUEST>-<JOBNUM>'.",
46 type
=> 'string', format
=> 'pve-replication-job-id',
47 pattern
=> '[1-9][0-9]{2,8}-\d{1,9}',
52 type
=> { description
=> "Section type." },
53 id
=> get_standard_option
('pve-replication-id'),
55 description
=> "Flag to disable/deactivate the entry.",
60 description
=> "Description.",
66 description
=> "Mark the replication job for removal. The job will remove all local"
67 ." replication snapshots. When set to 'full', it also tries to remove replicated"
68 ." volumes on the target. The job then removes itself from the configuration file.",
70 enum
=> ['local', 'full'],
74 description
=> "Rate limit in mbps (megabytes per second) as floating point number.",
80 description
=> "Storage replication schedule. The format is a subset of `systemd` calendar events.",
81 type
=> 'string', format
=> 'pve-calendar-event',
87 description
=> "For internal use, to detect if the guest was stolen.",
88 type
=> 'string', format
=> 'pve-node',
98 sub parse_section_header
{
99 my ($class, $line) = @_;
101 if ($line =~ m/^(\S+):\s*(\d+)-(\d+)\s*$/) {
102 my ($type, $guest, $subid) = (lc($1), int($2), int($3));
103 my $id = "$guest-$subid"; # use parsed integers
104 my $errmsg = undef; # set if you want to skip whole section
105 eval { parse_replication_job_id
($id); };
108 return ($type, $id, $errmsg, $config);
113 # Note: We want only one replication job per target to
114 # avoid confusion. This method should return a string
115 # which uniquely identifies the target.
116 sub get_unique_target_id
{
117 my ($class, $data) = @_;
119 die "please overwrite in subclass";
123 my ($class, $filename, $raw) = @_;
125 my $cfg = $class->SUPER::parse_config
($filename, $raw);
127 my $target_hash = {};
129 foreach my $id (sort keys %{$cfg->{ids
}}) {
130 my $data = $cfg->{ids
}->{$id};
132 my ($guest, $jobnum) = parse_replication_job_id
($id);
134 $data->{guest
} = $guest;
135 $data->{jobnum
} = $jobnum;
138 $data->{comment
} = PVE
::Tools
::decode_text
($data->{comment
})
139 if defined($data->{comment
});
141 my $plugin = $class->lookup($data->{type
});
142 my $tid = $plugin->get_unique_target_id($data);
143 my $vmid = $data->{guest
};
145 # should not happen, but we want to be sure
146 if (defined($target_hash->{$vmid}->{$tid})) {
147 warn "delete job $id: replication job for guest '$vmid' to target '$tid' already exists\n";
148 delete $cfg->{ids
}->{$id};
150 $target_hash->{$vmid}->{$tid} = 1;
157 my ($class, $filename, $cfg) = @_;
159 my $target_hash = {};
161 foreach my $id (keys %{$cfg->{ids
}}) {
162 my $data = $cfg->{ids
}->{$id};
164 my $plugin = $class->lookup($data->{type
});
165 my $tid = $plugin->get_unique_target_id($data);
166 my $vmid = $data->{guest
};
168 die "property 'guest' has wrong value\n" if $id !~ m/^\Q$vmid\E-/;
169 die "replication job for guest '$vmid' to target '$tid' already exists\n"
170 if defined($target_hash->{$vmid}->{$tid});
171 $target_hash->{$vmid}->{$tid} = 1;
173 $data->{comment
} = PVE
::Tools
::encode_text
($data->{comment
})
174 if defined($data->{comment
});
177 return $class->SUPER::write_config
($filename, $cfg);
183 my $class = ref($type) || $type;
185 my $cfg = cfs_read_file
($replication_cfg_filename);
187 return bless $cfg, $class;
193 cfs_write_file
($replication_cfg_filename, $cfg);
197 my ($code, $errmsg) = @_;
199 cfs_lock_file
($replication_cfg_filename, undef, $code);
202 $errmsg ?
die "$errmsg: $err" : die $err;
206 sub check_for_existing_jobs
{
207 my ($cfg, $vmid, $noerr) = @_;
209 foreach my $id (keys %{$cfg->{ids
}}) {
210 my $data = $cfg->{ids
}->{$id};
212 if ($data->{guest
} == $vmid) {
214 die "There is a replication job '$id' for guest '$vmid' - " .
215 "Please remove that first.\n"
222 sub find_local_replication_job
{
223 my ($cfg, $vmid, $target) = @_;
225 foreach my $id (keys %{$cfg->{ids
}}) {
226 my $data = $cfg->{ids
}->{$id};
228 return $data if $data->{type
} eq 'local' &&
229 $data->{guest
} == $vmid && $data->{target
} eq $target;
235 sub list_guests_local_replication_jobs
{
236 my ($cfg, $vmid) = @_;
240 for my $job (values %{$cfg->{ids
}}) {
241 next if $job->{type
} ne 'local' || $job->{guest
} != $vmid;
249 # makes old_target the new source for all local jobs of this guest
250 # makes new_target the target for the single local job with target old_target
251 sub switch_replication_job_target_nolock
{
252 my ($cfg, $vmid, $old_target, $new_target) = @_;
254 foreach my $jobcfg (values %{$cfg->{ids
}}) {
255 next if $jobcfg->{guest
} ne $vmid;
256 next if $jobcfg->{type
} ne 'local';
258 $jobcfg->{target
} = $new_target if $jobcfg->{target
} eq $old_target;
259 $jobcfg->{source
} = $old_target;
264 sub switch_replication_job_target
{
265 my ($vmid, $old_target, $new_target) = @_;
267 my $update_jobs = sub {
268 my $cfg = PVE
::ReplicationConfig-
>new();
269 $cfg->switch_replication_job_target_nolock($vmid, $old_target, $new_target);
278 my $cfg = __PACKAGE__-
>new();
279 delete $cfg->{ids
}->{$jobid};
286 sub remove_vmid_jobs
{
290 my $cfg = __PACKAGE__-
>new();
291 foreach my $id (keys %{$cfg->{ids
}}) {
292 delete $cfg->{ids
}->{$id} if ($cfg->{ids
}->{$id}->{guest
} == $vmid);
300 package PVE
::ReplicationConfig
::Cluster
;
302 use base
qw(PVE::ReplicationConfig);
311 description
=> "Target node.",
312 type
=> 'string', format
=> 'pve-node',
319 target
=> { fixed
=> 1, optional
=> 0 },
320 disable
=> { optional
=> 1 },
321 comment
=> { optional
=> 1 },
322 rate
=> { optional
=> 1 },
323 schedule
=> { optional
=> 1 },
324 remove_job
=> { optional
=> 1 },
325 source
=> { optional
=> 1 },
329 sub get_unique_target_id
{
330 my ($class, $data) = @_;
332 return "local/$data->{target}";
335 PVE
::ReplicationConfig
::Cluster-
>register();
336 PVE
::ReplicationConfig-
>init();