]>
Commit | Line | Data |
---|---|---|
892821fd DM |
1 | package PVE::CLI::pvesr; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use POSIX qw(strftime); | |
6 | use JSON; | |
7 | ||
8 | use PVE::JSONSchema qw(get_standard_option); | |
9 | use PVE::INotify; | |
10 | use PVE::RPCEnvironment; | |
11 | use PVE::Tools qw(extract_param); | |
12 | use PVE::SafeSyslog; | |
13 | use PVE::CLIHandler; | |
14 | ||
fae99506 | 15 | use PVE::Cluster; |
892821fd DM |
16 | use PVE::Replication; |
17 | use PVE::API2::ReplicationConfig; | |
18 | use PVE::API2::Replication; | |
19 | ||
20 | use base qw(PVE::CLIHandler); | |
21 | ||
22 | my $nodename = PVE::INotify::nodename(); | |
23 | ||
24 | sub setup_environment { | |
25 | PVE::RPCEnvironment->setup_default_cli_env(); | |
26 | } | |
27 | ||
91ee6a2f DM |
28 | # fixme: get from plugin?? |
29 | my $replicatable_storage_types = { | |
30 | zfspool => 1, | |
31 | }; | |
32 | ||
33 | my $check_wanted_volid = sub { | |
34 | my ($storecfg, $vmid, $volid, $local_node) = @_; | |
35 | ||
36 | my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid); | |
37 | my $scfg = PVE::Storage::storage_check_enabled($storecfg, $storeid, $local_node); | |
38 | die "storage '$storeid' is not replicatable\n" | |
39 | if !$replicatable_storage_types->{$scfg->{type}}; | |
40 | ||
41 | my ($vtype, undef, $ownervm) = PVE::Storage::parse_volname($storecfg, $volid); | |
42 | die "volume '$volid' has wrong vtype ($vtype != 'images')\n" | |
43 | if $vtype ne 'images'; | |
44 | die "volume '$volid' has wrong owner\n" | |
45 | if !$ownervm || $vmid != $ownervm; | |
46 | ||
47 | return $storeid; | |
48 | }; | |
49 | ||
fae99506 DM |
50 | __PACKAGE__->register_method ({ |
51 | name => 'prepare_local_job', | |
52 | path => 'prepare_local_job', | |
53 | method => 'POST', | |
54 | description => "Prepare for starting a replication job. This is called on the target node before replication starts. This call is for internal use, and return a JSON object on stdout. The method first test if VM <vmid> reside on the local node. If so, stop immediately. After that the method scans all volume IDs for snapshots, and removes all replications snapshots with timestamps different than <last_sync>. It also removes any unused volumes. Returns a hash with boolean markers for all volumes with existing replication snapshots.", | |
55 | parameters => { | |
56 | additionalProperties => 0, | |
57 | properties => { | |
58 | id => get_standard_option('pve-replication-id'), | |
fae99506 DM |
59 | 'extra-args' => get_standard_option('extra-args', { |
60 | description => "The list of volume IDs to consider." }), | |
91ee6a2f DM |
61 | scan => { |
62 | description => "List of storage IDs to scan for stale volumes.", | |
63 | type => 'string', format => 'pve-storage-id-list', | |
64 | optional => 1, | |
65 | }, | |
f9d38c54 DM |
66 | force => { |
67 | description => "Allow to remove all existion volumes (empty volume list).", | |
68 | type => 'boolean', | |
69 | optional => 1, | |
70 | default => 0, | |
71 | }, | |
fae99506 DM |
72 | last_sync => { |
73 | description => "Time (UNIX epoch) of last successful sync. If not specified, all replication snapshots get removed.", | |
74 | type => 'integer', | |
75 | minimum => 0, | |
76 | optional => 1, | |
77 | }, | |
91ee6a2f DM |
78 | parent_snapname => get_standard_option('pve-snapshot-name', { |
79 | optional => 1, | |
80 | }), | |
fae99506 DM |
81 | }, |
82 | }, | |
83 | returns => { type => 'null' }, | |
84 | code => sub { | |
85 | my ($param) = @_; | |
86 | ||
91ee6a2f DM |
87 | my $logfunc = sub { |
88 | my ($msg) = @_; | |
89 | print STDERR "$msg\n"; | |
90 | }; | |
fae99506 DM |
91 | |
92 | my $local_node = PVE::INotify::nodename(); | |
93 | ||
91ee6a2f DM |
94 | die "no volumes specified\n" |
95 | if !$param->{force} && !scalar(@{$param->{'extra-args'}}); | |
96 | ||
97 | my ($vmid, undef, $jobid) = PVE::ReplicationConfig::parse_replication_job_id($param->{id}); | |
98 | ||
fae99506 DM |
99 | my $vms = PVE::Cluster::get_vmlist(); |
100 | die "guest '$vmid' is on local node\n" | |
101 | if $vms->{ids}->{$vmid} && $vms->{ids}->{$vmid}->{node} eq $local_node; | |
102 | ||
91ee6a2f DM |
103 | my $last_sync = $param->{last_sync} // 0; |
104 | my $parent_snapname = $param->{parent_snapname}; | |
fae99506 | 105 | |
91ee6a2f | 106 | my $storecfg = PVE::Storage::config(); |
fae99506 | 107 | |
91ee6a2f DM |
108 | # compute list of storages we want to scan |
109 | my $storage_hash = {}; | |
110 | foreach my $storeid (PVE::Tools::split_list($param->{scan})) { | |
111 | my $scfg = PVE::Storage::storage_check_enabled($storecfg, $storeid, $local_node, 1); | |
112 | next if !$scfg; # simply ignore unavailable storages here | |
113 | die "storage '$storeid' is not replicatable\n" if !$replicatable_storage_types->{$scfg->{type}}; | |
114 | $storage_hash->{$storeid} = 1; | |
115 | } | |
fae99506 | 116 | |
91ee6a2f | 117 | my $wanted_volids = {}; |
fae99506 | 118 | foreach my $volid (@{$param->{'extra-args'}}) { |
91ee6a2f DM |
119 | my $storeid = $check_wanted_volid->($storecfg, $vmid, $volid, $local_node); |
120 | $wanted_volids->{$volid} = 1; | |
121 | $storage_hash->{$storeid} = 1; | |
fae99506 | 122 | } |
91ee6a2f DM |
123 | my $storage_list = [ sort keys %$storage_hash ]; |
124 | ||
125 | # activate all used storage | |
126 | my $cache = {}; | |
127 | PVE::Storage::activate_storage_list($storecfg, $storage_list, $cache); | |
128 | ||
129 | my $snapname = PVE::ReplicationState::replication_snapshot_name($jobid, $last_sync); | |
130 | ||
131 | # find replication snapshots | |
544bb4e2 | 132 | my $volids = []; |
91ee6a2f DM |
133 | foreach my $storeid (@$storage_list) { |
134 | my $scfg = PVE::Storage::storage_config($storecfg, $storeid); | |
135 | my $plugin = PVE::Storage::Plugin->lookup($scfg->{type}); | |
37efc934 | 136 | my $images = $plugin->list_images($storeid, $scfg, $vmid, undef, $cache); |
544bb4e2 FG |
137 | push @$volids, map { $_->{volid} } @$images; |
138 | } | |
139 | my ($last_snapshots, $cleaned_replicated_volumes) = PVE::Replication::prepare($storecfg, $volids, $jobid, $last_sync, $parent_snapname, $logfunc); | |
140 | foreach my $volid (keys %$cleaned_replicated_volumes) { | |
141 | if (!$wanted_volids->{$volid}) { | |
142 | $logfunc->("$jobid: delete stale volume '$volid'"); | |
143 | PVE::Storage::vdisk_free($storecfg, $volid); | |
144 | delete $last_snapshots->{$volid}; | |
fae99506 DM |
145 | } |
146 | } | |
147 | ||
fae99506 DM |
148 | print to_json($last_snapshots) . "\n"; |
149 | ||
150 | return undef; | |
151 | }}); | |
152 | ||
fcc22c0b DM |
153 | __PACKAGE__->register_method ({ |
154 | name => 'finalize_local_job', | |
155 | path => 'finalize_local_job', | |
156 | method => 'POST', | |
157 | description => "Finalize a replication job. This removes all replications snapshots with timestamps different than <last_sync>.", | |
158 | parameters => { | |
159 | additionalProperties => 0, | |
160 | properties => { | |
161 | id => get_standard_option('pve-replication-id'), | |
fcc22c0b DM |
162 | 'extra-args' => get_standard_option('extra-args', { |
163 | description => "The list of volume IDs to consider." }), | |
164 | last_sync => { | |
165 | description => "Time (UNIX epoch) of last successful sync. If not specified, all replication snapshots gets removed.", | |
166 | type => 'integer', | |
167 | minimum => 0, | |
168 | optional => 1, | |
169 | }, | |
170 | }, | |
171 | }, | |
172 | returns => { type => 'null' }, | |
173 | code => sub { | |
174 | my ($param) = @_; | |
175 | ||
a9da300d | 176 | my ($vmid, undef, $jobid) = PVE::ReplicationConfig::parse_replication_job_id($param->{id}); |
fcc22c0b DM |
177 | my $last_sync = $param->{last_sync} // 0; |
178 | ||
179 | my $local_node = PVE::INotify::nodename(); | |
180 | ||
181 | my $vms = PVE::Cluster::get_vmlist(); | |
182 | die "guest '$vmid' is on local node\n" | |
183 | if $vms->{ids}->{$vmid} && $vms->{ids}->{$vmid}->{node} eq $local_node; | |
184 | ||
185 | my $storecfg = PVE::Storage::config(); | |
186 | ||
187 | my $volids = []; | |
188 | ||
189 | die "no volumes specified\n" if !scalar(@{$param->{'extra-args'}}); | |
190 | ||
191 | foreach my $volid (@{$param->{'extra-args'}}) { | |
91ee6a2f | 192 | $check_wanted_volid->($storecfg, $vmid, $volid, $local_node); |
fcc22c0b DM |
193 | push @$volids, $volid; |
194 | } | |
195 | ||
196 | $volids = [ sort @$volids ]; | |
197 | ||
198 | my $logfunc = sub { | |
c364b61f | 199 | my ($msg) = @_; |
fcc22c0b DM |
200 | print STDERR "$msg\n"; |
201 | }; | |
202 | ||
203 | my $last_snapshots = PVE::Replication::prepare( | |
204 | $storecfg, $volids, $jobid, $last_sync, undef, $logfunc); | |
205 | ||
206 | return undef; | |
207 | }}); | |
208 | ||
892821fd DM |
209 | __PACKAGE__->register_method ({ |
210 | name => 'run', | |
211 | path => 'run', | |
212 | method => 'POST', | |
213 | description => "This method is called by the systemd-timer and executes all (or a specific) sync jobs.", | |
214 | parameters => { | |
215 | additionalProperties => 0, | |
216 | properties => { | |
217 | id => get_standard_option('pve-replication-id', { optional => 1 }), | |
f70997ea DM |
218 | verbose => { |
219 | description => "Print more verbose logs to stdout.", | |
220 | type => 'boolean', | |
221 | default => 0, | |
222 | optional => 1, | |
223 | }, | |
64d39c2e WL |
224 | mail => { |
225 | description => "Send an email notification in case of a failure.", | |
226 | type => 'boolean', | |
227 | default => 0, | |
228 | optional => 1, | |
229 | }, | |
892821fd DM |
230 | }, |
231 | }, | |
232 | returns => { type => 'null' }, | |
233 | code => sub { | |
234 | my ($param) = @_; | |
235 | ||
64d39c2e WL |
236 | die "Mail and id are mutually exclusive!\n" |
237 | if $param->{id} && $param->{mail}; | |
238 | ||
f70997ea DM |
239 | my $logfunc; |
240 | ||
241 | if ($param->{verbose}) { | |
242 | $logfunc = sub { | |
c364b61f | 243 | my ($msg) = @_; |
f70997ea DM |
244 | print "$msg\n"; |
245 | }; | |
246 | } | |
247 | ||
892821fd DM |
248 | if (my $id = extract_param($param, 'id')) { |
249 | ||
810c6776 | 250 | PVE::API2::Replication::run_single_job($id, undef, $logfunc); |
892821fd DM |
251 | |
252 | } else { | |
253 | ||
64d39c2e | 254 | PVE::API2::Replication::run_jobs(undef, $logfunc, 0, $param->{mail}); |
892821fd DM |
255 | } |
256 | ||
257 | return undef; | |
258 | }}); | |
259 | ||
260 | __PACKAGE__->register_method ({ | |
261 | name => 'enable', | |
262 | path => 'enable', | |
263 | method => 'POST', | |
264 | description => "Enable a replication job.", | |
265 | parameters => { | |
266 | additionalProperties => 0, | |
267 | properties => { | |
268 | id => get_standard_option('pve-replication-id'), | |
269 | }, | |
270 | }, | |
271 | returns => { type => 'null' }, | |
272 | code => sub { | |
273 | my ($param) = @_; | |
274 | ||
275 | $param->{disable} = 0; | |
276 | ||
277 | return PVE::API2::ReplicationConfig->update($param); | |
278 | }}); | |
279 | ||
280 | __PACKAGE__->register_method ({ | |
281 | name => 'disable', | |
282 | path => 'disable', | |
283 | method => 'POST', | |
284 | description => "Disable a replication job.", | |
285 | parameters => { | |
286 | additionalProperties => 0, | |
287 | properties => { | |
288 | id => get_standard_option('pve-replication-id'), | |
289 | }, | |
290 | }, | |
291 | returns => { type => 'null' }, | |
292 | code => sub { | |
293 | my ($param) = @_; | |
294 | ||
295 | $param->{disable} = 1; | |
296 | ||
297 | return PVE::API2::ReplicationConfig->update($param); | |
298 | }}); | |
299 | ||
fdc8b769 WL |
300 | __PACKAGE__->register_method ({ |
301 | name => 'set_state', | |
302 | path => '', | |
303 | protected => 1, | |
304 | method => 'POST', | |
305 | description => "Set the job replication state on migration. This call is for internal use. It will accept the job state as ja JSON obj.", | |
306 | permissions => { | |
307 | check => ['perm', '/storage', ['Datastore.Allocate']], | |
308 | }, | |
309 | parameters => { | |
310 | additionalProperties => 0, | |
311 | properties => { | |
312 | vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_vmid }), | |
313 | state => { | |
314 | description => "Job state as JSON decoded string.", | |
315 | type => 'string', | |
316 | }, | |
317 | }, | |
318 | }, | |
319 | returns => { type => 'null' }, | |
320 | code => sub { | |
321 | my ($param) = @_; | |
322 | ||
323 | my $vmid = extract_param($param, 'vmid'); | |
324 | my $json_string = extract_param($param, 'state'); | |
325 | my $remote_job_state= decode_json $json_string; | |
326 | ||
327 | PVE::ReplicationState::write_vmid_job_states($remote_job_state, $vmid); | |
328 | return undef; | |
329 | }}); | |
330 | ||
892821fd DM |
331 | my $print_job_list = sub { |
332 | my ($list) = @_; | |
333 | ||
a9da300d | 334 | my $format = "%-20s %-20s %10s %5s %8s\n"; |
892821fd | 335 | |
a9da300d | 336 | printf($format, "JobID", "Target", "Schedule", "Rate", "Enabled"); |
892821fd DM |
337 | |
338 | foreach my $job (sort { $a->{guest} <=> $b->{guest} } @$list) { | |
339 | my $plugin = PVE::ReplicationConfig->lookup($job->{type}); | |
340 | my $tid = $plugin->get_unique_target_id($job); | |
341 | ||
a9da300d | 342 | printf($format, $job->{id}, $tid, |
5c180db3 | 343 | defined($job->{schedule}) ? $job->{schedule} : '*/15', |
892821fd DM |
344 | defined($job->{rate}) ? $job->{rate} : '-', |
345 | $job->{disable} ? 'no' : 'yes' | |
346 | ); | |
347 | } | |
348 | }; | |
349 | ||
350 | my $print_job_status = sub { | |
351 | my ($list) = @_; | |
352 | ||
0cfec28c | 353 | my $format = "%-10s %-10s %-20s %20s %20s %10s %10s %s\n"; |
892821fd | 354 | |
0cfec28c | 355 | printf($format, "JobID", "Enabled", "Target", "LastSync", "NextSync", "Duration", "FailCount", "State"); |
892821fd DM |
356 | |
357 | foreach my $job (sort { $a->{guest} <=> $b->{guest} } @$list) { | |
358 | my $plugin = PVE::ReplicationConfig->lookup($job->{type}); | |
359 | my $tid = $plugin->get_unique_target_id($job); | |
360 | ||
5c180db3 DM |
361 | my $timestr = '-'; |
362 | if ($job->{last_sync}) { | |
363 | $timestr = strftime("%Y-%m-%d_%H:%M:%S", localtime($job->{last_sync})); | |
364 | } | |
365 | ||
366 | my $nextstr = '-'; | |
367 | if (my $next = $job->{next_sync}) { | |
368 | my $now = time(); | |
369 | if ($next > $now) { | |
370 | $nextstr = strftime("%Y-%m-%d_%H:%M:%S", localtime($job->{next_sync})); | |
371 | } else { | |
0b142737 | 372 | $nextstr = 'pending'; |
5c180db3 DM |
373 | } |
374 | } | |
892821fd | 375 | |
483f89dd | 376 | my $state = $job->{pid} ? "SYNCING" : $job->{error} // 'OK'; |
0cfec28c | 377 | my $enabled = $job->{disable} ? 'No' : 'Yes'; |
483f89dd | 378 | |
0cfec28c | 379 | printf($format, $job->{id}, $enabled, $tid, |
5c180db3 | 380 | $timestr, $nextstr, $job->{duration} // '-', |
483f89dd | 381 | $job->{fail_count}, $state); |
892821fd DM |
382 | } |
383 | }; | |
384 | ||
385 | our $cmddef = { | |
386 | status => [ 'PVE::API2::Replication', 'status', [], { node => $nodename }, $print_job_status ], | |
ca7d0707 | 387 | 'schedule-now' => [ 'PVE::API2::Replication', 'schedule_now', ['id'], { node => $nodename }], |
892821fd | 388 | |
5c180db3 | 389 | list => [ 'PVE::API2::ReplicationConfig', 'index' , [], {}, $print_job_list ], |
892821fd DM |
390 | read => [ 'PVE::API2::ReplicationConfig', 'read' , ['id'], {}, |
391 | sub { my $res = shift; print to_json($res, { utf8 => 1, pretty => 1, canonical => 1}); }], | |
392 | update => [ 'PVE::API2::ReplicationConfig', 'update' , ['id'], {} ], | |
393 | delete => [ 'PVE::API2::ReplicationConfig', 'delete' , ['id'], {} ], | |
a9da300d | 394 | 'create-local-job' => [ 'PVE::API2::ReplicationConfig', 'create' , ['id', 'target'], |
892821fd DM |
395 | { type => 'local' } ], |
396 | ||
397 | enable => [ __PACKAGE__, 'enable', ['id'], {}], | |
398 | disable => [ __PACKAGE__, 'disable', ['id'], {}], | |
399 | ||
a9da300d DM |
400 | 'prepare-local-job' => [ __PACKAGE__, 'prepare_local_job', ['id', 'extra-args'], {} ], |
401 | 'finalize-local-job' => [ __PACKAGE__, 'finalize_local_job', ['id', 'extra-args'], {} ], | |
fae99506 | 402 | |
892821fd | 403 | run => [ __PACKAGE__ , 'run'], |
fdc8b769 | 404 | 'set-state' => [ __PACKAGE__ , 'set_state', ['vmid', 'state']], |
892821fd DM |
405 | }; |
406 | ||
407 | 1; |