]>
Commit | Line | Data |
---|---|---|
58a3c91c FG |
1 | package PVE::AbstractConfig; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::Tools qw(lock_file lock_file_full); | |
7 | use PVE::INotify; | |
8 | use PVE::Cluster; | |
9 | use PVE::Storage; | |
10 | ||
67b3581e | 11 | use PVE::GuestHelpers qw(typesafe_ne); |
14f17b49 DM |
12 | use PVE::ReplicationConfig; |
13 | use PVE::Replication; | |
14 | ||
58a3c91c FG |
15 | my $nodename = PVE::INotify::nodename(); |
16 | ||
17 | # Printable string, currently either "VM" or "CT" | |
18 | sub guest_type { | |
19 | my ($class) = @_; | |
20 | die "abstract method - implement me "; | |
21 | } | |
22 | ||
23 | sub __config_max_unused_disks { | |
24 | my ($class) = @_; | |
25 | ||
26 | die "implement me - abstract method\n"; | |
27 | } | |
28 | ||
29 | # Path to the flock file for this VM/CT | |
30 | sub config_file_lock { | |
31 | my ($class, $vmid) = @_; | |
32 | die "abstract method - implement me"; | |
33 | } | |
34 | ||
35 | # Relative config file path for this VM/CT in CFS | |
36 | sub cfs_config_path { | |
37 | my ($class, $vmid, $node) = @_; | |
38 | die "abstract method - implement me"; | |
39 | } | |
40 | ||
41 | # Absolute config file path for this VM/CT | |
42 | sub config_file { | |
43 | my ($class, $vmid, $node) = @_; | |
44 | ||
45 | my $cfspath = $class->cfs_config_path($vmid, $node); | |
46 | return "/etc/pve/$cfspath"; | |
47 | } | |
48 | ||
49 | # Read and parse config file for this VM/CT | |
50 | sub load_config { | |
51 | my ($class, $vmid, $node) = @_; | |
52 | ||
53 | $node = $nodename if !$node; | |
54 | my $cfspath = $class->cfs_config_path($vmid, $node); | |
55 | ||
56 | my $conf = PVE::Cluster::cfs_read_file($cfspath); | |
57 | die "Configuration file '$cfspath' does not exist\n" | |
58 | if !defined($conf); | |
59 | ||
60 | return $conf; | |
61 | } | |
62 | ||
63 | # Generate and write config file for this VM/CT | |
64 | sub write_config { | |
65 | my ($class, $vmid, $conf) = @_; | |
66 | ||
67 | my $cfspath = $class->cfs_config_path($vmid); | |
68 | ||
69 | PVE::Cluster::cfs_write_file($cfspath, $conf); | |
70 | } | |
71 | ||
810ee088 OB |
72 | # Pending changes related |
73 | ||
74 | sub parse_pending_delete { | |
75 | my ($class, $data) = @_; | |
7b9d1ade TL |
76 | |
77 | return {} if !$data; | |
78 | ||
810ee088 OB |
79 | $data =~ s/[,;]/ /g; |
80 | $data =~ s/^\s+//; | |
7b9d1ade TL |
81 | |
82 | my $pending_deletions = {}; | |
83 | for my $entry (split(/\s+/, $data)) { | |
84 | my ($force, $key) = $entry =~ /^(!?)(.*)$/; | |
85 | ||
86 | $pending_deletions->{$key} = { | |
87 | force => $force ? 1 : 0, | |
88 | }; | |
89 | } | |
90 | ||
91 | return $pending_deletions; | |
810ee088 OB |
92 | } |
93 | ||
94 | sub print_pending_delete { | |
95 | my ($class, $delete_hash) = @_; | |
51088c75 TL |
96 | |
97 | my $render_key = sub { | |
98 | my $key = shift; | |
99 | $key = "!$key" if $delete_hash->{$key}->{force}; | |
100 | return $key; | |
101 | }; | |
102 | ||
6a6ad855 | 103 | join (',', map { $render_key->($_) } sort keys %$delete_hash); |
810ee088 OB |
104 | } |
105 | ||
106 | sub add_to_pending_delete { | |
107 | my ($class, $conf, $key, $force) = @_; | |
108 | ||
51e827b8 TL |
109 | $conf->{pending} //= {}; |
110 | my $pending = $conf->{pending}; | |
111 | my $pending_delete_hash = $class->parse_pending_delete($pending->{delete}); | |
112 | ||
113 | $pending_delete_hash->{$key} = { force => $force }; | |
114 | ||
115 | $pending->{delete} = $class->print_pending_delete($pending_delete_hash); | |
116 | ||
117 | return $conf; | |
810ee088 OB |
118 | } |
119 | ||
120 | sub remove_from_pending_delete { | |
121 | my ($class, $conf, $key) = @_; | |
122 | ||
464a42c4 TL |
123 | my $pending = $conf->{pending}; |
124 | my $pending_delete_hash = $class->parse_pending_delete($pending->{delete}); | |
125 | ||
126 | return $conf if ! exists $pending_delete_hash->{$key}; | |
127 | ||
810ee088 OB |
128 | delete $pending_delete_hash->{$key}; |
129 | ||
130 | if (%$pending_delete_hash) { | |
464a42c4 | 131 | $pending->{delete} = $class->print_pending_delete($pending_delete_hash); |
810ee088 | 132 | } else { |
464a42c4 | 133 | delete $pending->{delete}; |
810ee088 | 134 | } |
464a42c4 TL |
135 | |
136 | return $conf; | |
810ee088 OB |
137 | } |
138 | ||
139 | sub cleanup_pending { | |
140 | my ($class, $conf) = @_; | |
141 | ||
c24919a9 | 142 | my $pending = $conf->{pending}; |
810ee088 OB |
143 | # remove pending changes when nothing changed |
144 | my $changes; | |
145 | foreach my $opt (keys %{$conf->{pending}}) { | |
146 | next if $opt eq 'delete'; # just to be sure | |
9420142c | 147 | if (defined($conf->{$opt}) && ($pending->{$opt} eq $conf->{$opt})) { |
810ee088 | 148 | $changes = 1; |
c24919a9 | 149 | delete $pending->{$opt}; |
810ee088 OB |
150 | } |
151 | } | |
152 | ||
c24919a9 | 153 | my $current_delete_hash = $class->parse_pending_delete($pending->{delete}); |
810ee088 | 154 | my $pending_delete_hash = {}; |
c24919a9 | 155 | for my $opt (keys %$current_delete_hash) { |
810ee088 | 156 | if (defined($conf->{$opt})) { |
c24919a9 | 157 | $pending_delete_hash->{$opt} = $current_delete_hash->{$opt}; |
810ee088 OB |
158 | } else { |
159 | $changes = 1; | |
160 | } | |
161 | } | |
162 | ||
163 | if (%$pending_delete_hash) { | |
c24919a9 | 164 | $pending->{delete} = $class->print_pending_delete($pending_delete_hash); |
810ee088 | 165 | } else { |
c24919a9 | 166 | delete $pending->{delete}; |
810ee088 OB |
167 | } |
168 | ||
169 | return $changes; | |
170 | } | |
171 | ||
866f5834 OB |
172 | sub get_partial_fast_plug_option { |
173 | my ($class) = @_; | |
174 | ||
175 | die "abstract method - implement me "; | |
176 | } | |
177 | ||
178 | sub partial_fast_plug { | |
179 | my ($class, $conf, $opt) = @_; | |
180 | ||
181 | my $partial_fast_plug_option = $class->get_partial_fast_plug_option(); | |
47d7954c TL |
182 | return 0 if !$partial_fast_plug_option->{$opt}; |
183 | ||
866f5834 OB |
184 | my $format = $partial_fast_plug_option->{$opt}->{fmt}; |
185 | my $fast_pluggable = $partial_fast_plug_option->{$opt}->{properties}; | |
186 | ||
187 | my $configured = {}; | |
188 | if (exists($conf->{$opt})) { | |
189 | $configured = PVE::JSONSchema::parse_property_string($format, $conf->{$opt}); | |
190 | } | |
191 | my $pending = PVE::JSONSchema::parse_property_string($format, $conf->{pending}->{$opt}); | |
192 | ||
193 | my $changes = 0; | |
194 | ||
195 | # merge configured and pending opts to iterate | |
196 | my @all_keys = keys %{{ %$pending, %$configured }}; | |
197 | ||
198 | foreach my $subopt (@all_keys) { | |
199 | my $type = $format->{$subopt}->{type}; | |
67b3581e | 200 | if (typesafe_ne($configured->{$subopt}, $pending->{$subopt}, $type)) { |
866f5834 OB |
201 | if ($fast_pluggable->{$subopt}) { |
202 | $configured->{$subopt} = $pending->{$subopt}; | |
203 | $changes = 1 | |
204 | } | |
205 | } | |
206 | } | |
207 | ||
208 | # if there're no keys in $configured (after merge) there shouldn't be anything to change | |
209 | if (keys %$configured) { | |
210 | $conf->{$opt} = PVE::JSONSchema::print_property_string($configured, $format); | |
211 | } | |
212 | ||
213 | return $changes; | |
214 | } | |
215 | ||
216 | ||
61264e82 OB |
217 | sub load_snapshot_config { |
218 | my ($class, $vmid, $snapname) = @_; | |
219 | ||
220 | my $conf = $class->load_config($vmid); | |
221 | ||
222 | my $snapshot = $conf->{snapshots}->{$snapname}; | |
223 | die "snapshot '$snapname' does not exist\n" if !defined($snapshot); | |
224 | ||
225 | $snapshot->{digest} = $conf->{digest}; | |
226 | ||
227 | return $snapshot; | |
228 | ||
229 | } | |
230 | ||
231 | sub load_current_config { | |
232 | my ($class, $vmid, $current) = @_; | |
233 | ||
234 | my $conf = $class->load_config($vmid); | |
235 | ||
236 | # take pending changes in | |
237 | if (!$current) { | |
238 | foreach my $opt (keys %{$conf->{pending}}) { | |
239 | next if $opt eq 'delete'; | |
240 | my $value = $conf->{pending}->{$opt}; | |
241 | next if ref($value); # just to be sure | |
242 | $conf->{$opt} = $value; | |
243 | } | |
244 | my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete}); | |
245 | foreach my $opt (keys %$pending_delete_hash) { | |
246 | delete $conf->{$opt} if $conf->{$opt}; | |
247 | } | |
248 | } | |
249 | ||
250 | delete $conf->{snapshots}; | |
251 | delete $conf->{pending}; | |
252 | ||
253 | return $conf; | |
254 | } | |
255 | ||
256 | ||
58a3c91c | 257 | # Lock config file using flock, run $code with @param, unlock config file. |
ab44df53 | 258 | # $timeout is the maximum time to acquire the flock |
58a3c91c FG |
259 | sub lock_config_full { |
260 | my ($class, $vmid, $timeout, $code, @param) = @_; | |
261 | ||
093c4c6e | 262 | return $class->lock_config_mode($vmid, $timeout, 0, $code, @param); |
58a3c91c FG |
263 | } |
264 | ||
cbbb06a5 | 265 | sub create_and_lock_config { |
fde98d5e | 266 | my ($class, $vmid, $allow_existing, $lock) = @_; |
cbbb06a5 TL |
267 | |
268 | $class->lock_config_full($vmid, 5, sub { | |
269 | PVE::Cluster::check_vmid_unused($vmid, $allow_existing); | |
270 | ||
271 | my $conf = eval { $class->load_config($vmid) } || {}; | |
272 | $class->check_lock($conf); | |
fde98d5e | 273 | $conf->{lock} = $lock // 'create'; |
cbbb06a5 TL |
274 | $class->write_config($vmid, $conf); |
275 | }); | |
276 | } | |
277 | ||
ab44df53 | 278 | # destroys configuration, only applicable for configs owned by the callers node. |
1aa76f2f TL |
279 | # dies if removal fails, e.g., when inquorate. |
280 | sub destroy_config { | |
281 | my ($class, $vmid) = @_; | |
282 | ||
283 | my $config_fn = $class->config_file($vmid, $nodename); | |
284 | unlink $config_fn or die "failed to remove config file: $!\n"; | |
285 | } | |
286 | ||
58a3c91c | 287 | # Lock config file using flock, run $code with @param, unlock config file. |
ab44df53 | 288 | # $timeout is the maximum time to acquire the flock |
58a3c91c FG |
289 | # $shared eq 1 creates a non-exclusive ("read") flock |
290 | sub lock_config_mode { | |
291 | my ($class, $vmid, $timeout, $shared, $code, @param) = @_; | |
292 | ||
293 | my $filename = $class->config_file_lock($vmid); | |
294 | ||
97b9ccec FE |
295 | # make sure configuration file is up-to-date |
296 | my $realcode = sub { | |
297 | PVE::Cluster::cfs_update(); | |
298 | $code->(@param); | |
299 | }; | |
300 | ||
301 | my $res = lock_file_full($filename, $timeout, $shared, $realcode, @param); | |
58a3c91c FG |
302 | |
303 | die $@ if $@; | |
304 | ||
305 | return $res; | |
306 | } | |
307 | ||
308 | # Lock config file using flock, run $code with @param, unlock config file. | |
309 | sub lock_config { | |
310 | my ($class, $vmid, $code, @param) = @_; | |
311 | ||
312 | return $class->lock_config_full($vmid, 10, $code, @param); | |
313 | } | |
314 | ||
315 | # Checks whether the config is locked with the lock parameter | |
316 | sub check_lock { | |
317 | my ($class, $conf) = @_; | |
318 | ||
319 | die $class->guest_type()." is locked ($conf->{lock})\n" if $conf->{lock}; | |
320 | } | |
321 | ||
322 | # Returns whether the config is locked with the lock parameter, also checks | |
323 | # whether the lock value is correct if the optional $lock is set. | |
324 | sub has_lock { | |
325 | my ($class, $conf, $lock) = @_; | |
326 | ||
327 | return $conf->{lock} && (!defined($lock) || $lock eq $conf->{lock}); | |
328 | } | |
329 | ||
330 | # Sets the lock parameter for this VM/CT's config to $lock. | |
331 | sub set_lock { | |
332 | my ($class, $vmid, $lock) = @_; | |
333 | ||
334 | my $conf; | |
335 | $class->lock_config($vmid, sub { | |
336 | $conf = $class->load_config($vmid); | |
337 | $class->check_lock($conf); | |
338 | $conf->{lock} = $lock; | |
339 | $class->write_config($vmid, $conf); | |
340 | }); | |
341 | return $conf; | |
342 | } | |
343 | ||
344 | # Removes the lock parameter for this VM/CT's config, also checks whether | |
345 | # the lock value is correct if the optional $lock is set. | |
346 | sub remove_lock { | |
347 | my ($class, $vmid, $lock) = @_; | |
348 | ||
349 | $class->lock_config($vmid, sub { | |
350 | my $conf = $class->load_config($vmid); | |
351 | if (!$conf->{lock}) { | |
352 | my $lockstring = defined($lock) ? "'$lock' " : "any"; | |
353 | die "no lock found trying to remove $lockstring lock\n"; | |
354 | } elsif (defined($lock) && $conf->{lock} ne $lock) { | |
355 | die "found lock '$conf->{lock}' trying to remove '$lock' lock\n"; | |
356 | } | |
357 | delete $conf->{lock}; | |
358 | $class->write_config($vmid, $conf); | |
359 | }); | |
360 | } | |
361 | ||
362 | # Checks whether protection mode is enabled for this VM/CT. | |
363 | sub check_protection { | |
364 | my ($class, $conf, $err_msg) = @_; | |
365 | ||
366 | if ($conf->{protection}) { | |
367 | die "$err_msg - protection mode enabled\n"; | |
368 | } | |
369 | } | |
370 | ||
4e6382ab FE |
371 | # Returns a list of keys where used volumes can be located |
372 | sub valid_volume_keys { | |
373 | my ($class, $reverse) = @_; | |
374 | ||
375 | die "implement me - abstract method\n"; | |
376 | } | |
377 | ||
378 | # Returns a hash with the information from $volume_string | |
379 | # $key is used to determine the format of the string | |
380 | sub parse_volume { | |
381 | my ($class, $key, $volume_string, $noerr) = @_; | |
382 | ||
383 | die "implement me - abstract method\n"; | |
384 | } | |
385 | ||
386 | # Returns a string with the information from $volume | |
387 | # $key is used to determine the format of the string | |
388 | sub print_volume { | |
389 | my ($class, $key, $volume) = @_; | |
390 | ||
391 | die "implement me - abstract method\n"; | |
392 | } | |
393 | ||
394 | # The key under which the volume ID is located in volume hashes | |
395 | sub volid_key { | |
396 | my ($class) = @_; | |
397 | ||
398 | die "implement me - abstract method\n"; | |
399 | } | |
400 | ||
58a3c91c FG |
401 | # Adds an unused volume to $config, if possible. |
402 | sub add_unused_volume { | |
403 | my ($class, $config, $volid) = @_; | |
404 | ||
405 | my $key; | |
406 | for (my $ind = $class->__config_max_unused_disks() - 1; $ind >= 0; $ind--) { | |
407 | my $test = "unused$ind"; | |
408 | if (my $vid = $config->{$test}) { | |
409 | return if $vid eq $volid; # do not add duplicates | |
410 | } else { | |
411 | $key = $test; | |
412 | } | |
413 | } | |
414 | ||
415 | die "Too many unused volumes - please delete them first.\n" if !$key; | |
416 | ||
417 | $config->{$key} = $volid; | |
418 | ||
419 | return $key; | |
420 | } | |
421 | ||
8db01440 FE |
422 | # Iterate over all unused volumes, calling $func for each key/value pair |
423 | # with additional parameters @param. | |
424 | sub foreach_unused_volume { | |
425 | my ($class, $conf, $func, @param) = @_; | |
426 | ||
427 | foreach my $key (keys %{$conf}) { | |
428 | if ($key =~ m/^unused\d+$/) { | |
429 | my $volume = $class->parse_volume($key, $conf->{$key}); | |
430 | $func->($key, $volume, @param); | |
431 | } | |
432 | } | |
433 | } | |
434 | ||
261d47a4 FE |
435 | # Iterate over all configured volumes, calling $func for each key/value pair |
436 | # with additional parameters @param. | |
437 | # By default, unused volumes and specials like vmstate are excluded. | |
438 | # Options: reverse - reverses the order for the iteration | |
439 | # include_unused - also iterate over unused volumes | |
440 | # extra_keys - an array of extra keys to use for the iteration | |
441 | sub foreach_volume_full { | |
442 | my ($class, $conf, $opts, $func, @param) = @_; | |
443 | ||
444 | die "'reverse' iteration only supported for default keys\n" | |
445 | if $opts->{reverse} && ($opts->{extra_keys} || $opts->{include_unused}); | |
446 | ||
447 | my @keys = $class->valid_volume_keys($opts->{reverse}); | |
448 | push @keys, @{$opts->{extra_keys}} if $opts->{extra_keys}; | |
449 | ||
450 | foreach my $key (@keys) { | |
451 | my $volume_string = $conf->{$key}; | |
452 | next if !defined($volume_string); | |
453 | ||
454 | my $volume = $class->parse_volume($key, $volume_string, 1); | |
455 | next if !defined($volume); | |
456 | ||
457 | $func->($key, $volume, @param); | |
458 | } | |
459 | ||
460 | $class->foreach_unused_volume($conf, $func, @param) if $opts->{include_unused}; | |
461 | } | |
462 | ||
463 | sub foreach_volume { | |
464 | my ($class, $conf, $func, @param) = @_; | |
465 | ||
466 | return $class->foreach_volume_full($conf, undef, $func, @param); | |
467 | } | |
468 | ||
95a9508e FE |
469 | # $volume_map is a hash of 'old_volid' => 'new_volid' pairs. |
470 | # This method replaces 'old_volid' by 'new_volid' throughout | |
471 | # the config including snapshots and unused and vmstate volumes | |
472 | sub update_volume_ids { | |
473 | my ($class, $conf, $volume_map) = @_; | |
474 | ||
475 | my $volid_key = $class->volid_key(); | |
476 | ||
477 | my $do_replace = sub { | |
478 | my ($key, $volume, $current_section) = @_; | |
479 | ||
480 | my $old_volid = $volume->{$volid_key}; | |
481 | if (my $new_volid = $volume_map->{$old_volid}) { | |
482 | $volume->{$volid_key} = $new_volid; | |
483 | $current_section->{$key} = $class->print_volume($key, $volume); | |
484 | } | |
485 | }; | |
486 | ||
487 | my $opts = { | |
488 | 'include_unused' => 1, | |
489 | 'extra_keys' => ['vmstate'], | |
490 | }; | |
491 | ||
492 | $class->foreach_volume_full($conf, $opts, $do_replace, $conf); | |
493 | foreach my $snap (keys %{$conf->{snapshots}}) { | |
494 | my $snap_conf = $conf->{snapshots}->{$snap}; | |
495 | $class->foreach_volume_full($snap_conf, $opts, $do_replace, $snap_conf); | |
496 | } | |
497 | } | |
498 | ||
58a3c91c FG |
499 | # Returns whether the template parameter is set in $conf. |
500 | sub is_template { | |
501 | my ($class, $conf) = @_; | |
502 | ||
503 | return 1 if defined $conf->{template} && $conf->{template} == 1; | |
504 | } | |
505 | ||
ab44df53 | 506 | # Checks whether $feature is available for the referenced volumes in $conf. |
58a3c91c FG |
507 | # Note: depending on the parameters, some volumes may be skipped! |
508 | sub has_feature { | |
509 | my ($class, $feature, $conf, $storecfg, $snapname, $running, $backup_only) = @_; | |
510 | die "implement me - abstract method\n"; | |
511 | } | |
512 | ||
3f85b14d DM |
513 | # get all replicatable volume (hash $res->{$volid} = 1) |
514 | # $cleanup: for cleanup - simply ignores volumes without replicate feature | |
515 | # $norerr: never raise exceptions - return undef instead | |
516 | sub get_replicatable_volumes { | |
54b79ff5 | 517 | my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_; |
3f85b14d DM |
518 | |
519 | die "implement me - abstract method\n"; | |
520 | } | |
521 | ||
1245ad85 TL |
522 | # Returns all the guests volumes which would be included in a vzdump job |
523 | # Return Format (array-ref with hash-refs as elements): | |
b2a2affa AL |
524 | # [ |
525 | # { | |
1245ad85 TL |
526 | # key, key in the config, e.g. mp0, scsi0,... |
527 | # included, boolean | |
528 | # reason, string | |
529 | # data volume object as returned from foreach_drive/foreach_mountpoint | |
530 | # }, | |
b2a2affa AL |
531 | # ] |
532 | sub get_backup_volumes { | |
533 | my ($class, $conf) = @_; | |
534 | ||
535 | die "implement me - abstract method\n"; | |
536 | } | |
537 | ||
58a3c91c FG |
538 | # Internal snapshots |
539 | ||
540 | # NOTE: Snapshot create/delete involves several non-atomic | |
541 | # actions, and can take a long time. | |
542 | # So we try to avoid locking the file and use the 'lock' variable | |
543 | # inside the config file instead. | |
544 | ||
545 | # Save the vmstate (RAM). | |
546 | sub __snapshot_save_vmstate { | |
547 | my ($class, $vmid, $conf, $snapname, $storecfg) = @_; | |
548 | die "implement me - abstract method\n"; | |
549 | } | |
550 | ||
551 | # Check whether the VM/CT is running. | |
552 | sub __snapshot_check_running { | |
553 | my ($class, $vmid) = @_; | |
554 | die "implement me - abstract method\n"; | |
555 | } | |
556 | ||
557 | # Check whether we need to freeze the VM/CT | |
558 | sub __snapshot_check_freeze_needed { | |
559 | my ($sself, $vmid, $config, $save_vmstate) = @_; | |
560 | die "implement me - abstract method\n"; | |
561 | } | |
562 | ||
563 | # Freeze or unfreeze the VM/CT. | |
564 | sub __snapshot_freeze { | |
565 | my ($class, $vmid, $unfreeze) = @_; | |
566 | ||
567 | die "abstract method - implement me\n"; | |
568 | } | |
569 | ||
570 | # Code run before and after creating all the volume snapshots | |
571 | # base: noop | |
572 | sub __snapshot_create_vol_snapshots_hook { | |
573 | my ($class, $vmid, $snap, $running, $hook) = @_; | |
574 | ||
575 | return; | |
576 | } | |
577 | ||
578 | # Create the volume snapshots for the VM/CT. | |
579 | sub __snapshot_create_vol_snapshot { | |
580 | my ($class, $vmid, $vs, $volume, $snapname) = @_; | |
581 | ||
582 | die "abstract method - implement me\n"; | |
583 | } | |
584 | ||
585 | # Remove a drive from the snapshot config. | |
586 | sub __snapshot_delete_remove_drive { | |
587 | my ($class, $snap, $drive) = @_; | |
588 | ||
589 | die "abstract method - implement me\n"; | |
590 | } | |
591 | ||
592 | # Delete the vmstate file/drive | |
593 | sub __snapshot_delete_vmstate_file { | |
594 | my ($class, $snap, $force) = @_; | |
595 | ||
596 | die "abstract method - implement me\n"; | |
597 | } | |
598 | ||
599 | # Delete a volume snapshot | |
600 | sub __snapshot_delete_vol_snapshot { | |
601 | my ($class, $vmid, $vs, $volume, $snapname) = @_; | |
602 | ||
603 | die "abstract method - implement me\n"; | |
604 | } | |
605 | ||
3fa4ce45 DC |
606 | # called during rollback prepare, and between config rollback and starting guest |
607 | # can change config, e.g. for vmgenid | |
608 | # $data is shared across calls and passed to vm_start | |
609 | sub __snapshot_rollback_hook { | |
610 | my ($class, $vmid, $conf, $snap, $prepare, $data) = @_; | |
611 | ||
612 | return; | |
613 | } | |
614 | ||
58a3c91c FG |
615 | # Checks whether a volume snapshot is possible for this volume. |
616 | sub __snapshot_rollback_vol_possible { | |
617 | my ($class, $volume, $snapname) = @_; | |
618 | ||
619 | die "abstract method - implement me\n"; | |
620 | } | |
621 | ||
622 | # Rolls back this volume. | |
623 | sub __snapshot_rollback_vol_rollback { | |
624 | my ($class, $volume, $snapname) = @_; | |
625 | ||
626 | die "abstract method - implement me\n"; | |
627 | } | |
628 | ||
629 | # Stops the VM/CT for a rollback. | |
630 | sub __snapshot_rollback_vm_stop { | |
631 | my ($class, $vmid) = @_; | |
632 | ||
633 | die "abstract method - implement me\n"; | |
634 | } | |
635 | ||
636 | # Start the VM/CT after a rollback with restored vmstate. | |
637 | sub __snapshot_rollback_vm_start { | |
3fa4ce45 | 638 | my ($class, $vmid, $vmstate, $data); |
58a3c91c FG |
639 | |
640 | die "abstract method - implement me\n"; | |
641 | } | |
642 | ||
643 | # Get list of volume IDs which are referenced in $conf, but not in $snap. | |
644 | sub __snapshot_rollback_get_unused { | |
645 | my ($class, $conf, $snap) = @_; | |
646 | ||
647 | die "abstract method - implement me\n"; | |
648 | } | |
649 | ||
58a3c91c FG |
650 | # Copy the current config $source to the snapshot config $dest |
651 | sub __snapshot_copy_config { | |
652 | my ($class, $source, $dest) = @_; | |
653 | ||
654 | foreach my $k (keys %$source) { | |
655 | next if $k eq 'snapshots'; | |
656 | next if $k eq 'snapstate'; | |
657 | next if $k eq 'snaptime'; | |
658 | next if $k eq 'vmstate'; | |
659 | next if $k eq 'lock'; | |
660 | next if $k eq 'digest'; | |
661 | next if $k eq 'description'; | |
662 | next if $k =~ m/^unused\d+$/; | |
663 | ||
664 | $dest->{$k} = $source->{$k}; | |
665 | } | |
666 | }; | |
667 | ||
668 | # Apply the snapshot config $snap to the config $conf (rollback) | |
669 | sub __snapshot_apply_config { | |
670 | my ($class, $conf, $snap) = @_; | |
671 | ||
672 | # copy snapshot list | |
673 | my $newconf = { | |
674 | snapshots => $conf->{snapshots}, | |
675 | }; | |
676 | ||
eb74d483 | 677 | # keep description and list of unused disks |
58a3c91c | 678 | foreach my $k (keys %$conf) { |
eb74d483 | 679 | next if !($k =~ m/^unused\d+$/ || $k eq 'description'); |
38532f70 | 680 | |
58a3c91c FG |
681 | $newconf->{$k} = $conf->{$k}; |
682 | } | |
683 | ||
684 | $class->__snapshot_copy_config($snap, $newconf); | |
685 | ||
686 | return $newconf; | |
687 | } | |
688 | ||
689 | # Prepares the configuration for snapshotting. | |
690 | sub __snapshot_prepare { | |
691 | my ($class, $vmid, $snapname, $save_vmstate, $comment) = @_; | |
692 | ||
693 | my $snap; | |
694 | ||
695 | my $updatefn = sub { | |
696 | ||
697 | my $conf = $class->load_config($vmid); | |
698 | ||
699 | die "you can't take a snapshot if it's a template\n" | |
700 | if $class->is_template($conf); | |
701 | ||
702 | $class->check_lock($conf); | |
703 | ||
704 | $conf->{lock} = 'snapshot'; | |
705 | ||
706 | die "snapshot name '$snapname' already used\n" | |
707 | if defined($conf->{snapshots}->{$snapname}); | |
708 | ||
709 | my $storecfg = PVE::Storage::config(); | |
710 | die "snapshot feature is not available\n" | |
711 | if !$class->has_feature('snapshot', $conf, $storecfg, undef, undef, $snapname eq 'vzdump'); | |
712 | ||
713 | $snap = $conf->{snapshots}->{$snapname} = {}; | |
714 | ||
715 | if ($save_vmstate && $class->__snapshot_check_running($vmid)) { | |
716 | $class->__snapshot_save_vmstate($vmid, $conf, $snapname, $storecfg); | |
717 | } | |
718 | ||
719 | $class->__snapshot_copy_config($conf, $snap); | |
720 | ||
721 | $snap->{snapstate} = "prepare"; | |
722 | $snap->{snaptime} = time(); | |
723 | $snap->{description} = $comment if $comment; | |
724 | ||
725 | $class->write_config($vmid, $conf); | |
726 | }; | |
727 | ||
728 | $class->lock_config($vmid, $updatefn); | |
729 | ||
730 | return $snap; | |
731 | } | |
732 | ||
733 | # Commits the configuration after snapshotting. | |
734 | sub __snapshot_commit { | |
735 | my ($class, $vmid, $snapname) = @_; | |
736 | ||
737 | my $updatefn = sub { | |
738 | ||
739 | my $conf = $class->load_config($vmid); | |
740 | ||
741 | die "missing snapshot lock\n" | |
742 | if !($conf->{lock} && $conf->{lock} eq 'snapshot'); | |
743 | ||
744 | my $snap = $conf->{snapshots}->{$snapname}; | |
745 | die "snapshot '$snapname' does not exist\n" if !defined($snap); | |
746 | ||
747 | die "wrong snapshot state\n" | |
748 | if !($snap->{snapstate} && $snap->{snapstate} eq "prepare"); | |
749 | ||
750 | delete $snap->{snapstate}; | |
751 | delete $conf->{lock}; | |
752 | ||
753 | $conf->{parent} = $snapname; | |
754 | ||
755 | $class->write_config($vmid, $conf); | |
756 | }; | |
757 | ||
758 | $class->lock_config($vmid, $updatefn); | |
759 | } | |
760 | ||
761 | # Creates a snapshot for the VM/CT. | |
762 | sub snapshot_create { | |
763 | my ($class, $vmid, $snapname, $save_vmstate, $comment) = @_; | |
764 | ||
765 | my $snap = $class->__snapshot_prepare($vmid, $snapname, $save_vmstate, $comment); | |
766 | ||
767 | $save_vmstate = 0 if !$snap->{vmstate}; | |
768 | ||
769 | my $conf = $class->load_config($vmid); | |
770 | ||
771 | my ($running, $freezefs) = $class->__snapshot_check_freeze_needed($vmid, $conf, $snap->{vmstate}); | |
772 | ||
773 | my $drivehash = {}; | |
774 | ||
775 | eval { | |
776 | if ($freezefs) { | |
777 | $class->__snapshot_freeze($vmid, 0); | |
778 | } | |
779 | ||
780 | $class->__snapshot_create_vol_snapshots_hook($vmid, $snap, $running, "before"); | |
781 | ||
261d47a4 | 782 | $class->foreach_volume($snap, sub { |
58a3c91c FG |
783 | my ($vs, $volume) = @_; |
784 | ||
785 | $class->__snapshot_create_vol_snapshot($vmid, $vs, $volume, $snapname); | |
786 | $drivehash->{$vs} = 1; | |
787 | }); | |
788 | }; | |
789 | my $err = $@; | |
790 | ||
791 | if ($running) { | |
792 | $class->__snapshot_create_vol_snapshots_hook($vmid, $snap, $running, "after"); | |
793 | if ($freezefs) { | |
794 | $class->__snapshot_freeze($vmid, 1); | |
795 | } | |
796 | $class->__snapshot_create_vol_snapshots_hook($vmid, $snap, $running, "after-unfreeze"); | |
797 | } | |
798 | ||
799 | if ($err) { | |
800 | warn "snapshot create failed: starting cleanup\n"; | |
801 | eval { $class->snapshot_delete($vmid, $snapname, 1, $drivehash); }; | |
802 | warn "$@" if $@; | |
803 | die "$err\n"; | |
804 | } | |
805 | ||
806 | $class->__snapshot_commit($vmid, $snapname); | |
807 | } | |
808 | ||
809 | # Deletes a snapshot. | |
810 | # Note: $drivehash is only set when called from snapshot_create. | |
811 | sub snapshot_delete { | |
812 | my ($class, $vmid, $snapname, $force, $drivehash) = @_; | |
813 | ||
814 | my $prepare = 1; | |
815 | ||
58a3c91c FG |
816 | my $unused = []; |
817 | ||
4c3144ea AA |
818 | my $conf = $class->load_config($vmid); |
819 | my $snap = $conf->{snapshots}->{$snapname}; | |
820 | ||
821 | die "snapshot '$snapname' does not exist\n" if !defined($snap); | |
822 | ||
58a3c91c FG |
823 | $class->set_lock($vmid, 'snapshot-delete') |
824 | if (!$drivehash); # doesn't already have a 'snapshot' lock | |
825 | ||
826 | my $unlink_parent = sub { | |
827 | my ($confref, $new_parent) = @_; | |
828 | ||
829 | if ($confref->{parent} && $confref->{parent} eq $snapname) { | |
830 | if ($new_parent) { | |
831 | $confref->{parent} = $new_parent; | |
832 | } else { | |
833 | delete $confref->{parent}; | |
834 | } | |
835 | } | |
836 | }; | |
837 | ||
838 | my $remove_drive = sub { | |
839 | my ($drive) = @_; | |
840 | ||
841 | my $conf = $class->load_config($vmid); | |
842 | $snap = $conf->{snapshots}->{$snapname}; | |
843 | die "snapshot '$snapname' does not exist\n" if !defined($snap); | |
844 | ||
845 | $class->__snapshot_delete_remove_drive($snap, $drive); | |
846 | ||
847 | $class->write_config($vmid, $conf); | |
848 | }; | |
849 | ||
850 | #prepare | |
851 | $class->lock_config($vmid, sub { | |
852 | my $conf = $class->load_config($vmid); | |
853 | ||
854 | die "you can't delete a snapshot if vm is a template\n" | |
855 | if $class->is_template($conf); | |
856 | ||
857 | $snap = $conf->{snapshots}->{$snapname}; | |
858 | die "snapshot '$snapname' does not exist\n" if !defined($snap); | |
859 | ||
860 | $snap->{snapstate} = 'delete'; | |
861 | ||
862 | $class->write_config($vmid, $conf); | |
863 | }); | |
864 | ||
865 | # now remove vmstate file | |
866 | if ($snap->{vmstate}) { | |
867 | $class->__snapshot_delete_vmstate_file($snap, $force); | |
868 | ||
869 | # save changes (remove vmstate from snapshot) | |
870 | $class->lock_config($vmid, $remove_drive, 'vmstate') if !$force; | |
871 | }; | |
872 | ||
873 | # now remove all volume snapshots | |
261d47a4 | 874 | $class->foreach_volume($snap, sub { |
58a3c91c FG |
875 | my ($vs, $volume) = @_; |
876 | ||
877 | return if $snapname eq 'vzdump' && $vs ne 'rootfs' && !$volume->{backup}; | |
878 | if (!$drivehash || $drivehash->{$vs}) { | |
879 | eval { $class->__snapshot_delete_vol_snapshot($vmid, $vs, $volume, $snapname, $unused); }; | |
880 | if (my $err = $@) { | |
881 | die $err if !$force; | |
882 | warn $err; | |
883 | } | |
884 | } | |
885 | ||
886 | # save changes (remove drive from snapshot) | |
887 | $class->lock_config($vmid, $remove_drive, $vs) if !$force; | |
888 | }); | |
889 | ||
890 | # now cleanup config | |
891 | $class->lock_config($vmid, sub { | |
892 | my $conf = $class->load_config($vmid); | |
893 | $snap = $conf->{snapshots}->{$snapname}; | |
894 | die "snapshot '$snapname' does not exist\n" if !defined($snap); | |
895 | ||
896 | # remove parent refs | |
897 | &$unlink_parent($conf, $snap->{parent}); | |
898 | foreach my $sn (keys %{$conf->{snapshots}}) { | |
899 | next if $sn eq $snapname; | |
900 | &$unlink_parent($conf->{snapshots}->{$sn}, $snap->{parent}); | |
901 | } | |
902 | ||
903 | ||
904 | delete $conf->{snapshots}->{$snapname}; | |
905 | delete $conf->{lock}; | |
906 | foreach my $volid (@$unused) { | |
907 | $class->add_unused_volume($conf, $volid); | |
908 | } | |
909 | ||
910 | $class->write_config($vmid, $conf); | |
911 | }); | |
912 | } | |
913 | ||
914 | # Rolls back to a given snapshot. | |
915 | sub snapshot_rollback { | |
916 | my ($class, $vmid, $snapname) = @_; | |
917 | ||
918 | my $prepare = 1; | |
919 | ||
920 | my $storecfg = PVE::Storage::config(); | |
921 | ||
82a04857 | 922 | my $data = {}; |
58a3c91c FG |
923 | |
924 | my $get_snapshot_config = sub { | |
82a04857 | 925 | my ($conf) = @_; |
58a3c91c FG |
926 | |
927 | die "you can't rollback if vm is a template\n" if $class->is_template($conf); | |
928 | ||
929 | my $res = $conf->{snapshots}->{$snapname}; | |
930 | ||
931 | die "snapshot '$snapname' does not exist\n" if !defined($res); | |
932 | ||
933 | return $res; | |
934 | }; | |
935 | ||
82a04857 | 936 | my $snap; |
3fa4ce45 | 937 | |
58a3c91c | 938 | my $updatefn = sub { |
82a04857 FG |
939 | my $conf = $class->load_config($vmid); |
940 | $snap = $get_snapshot_config->($conf); | |
58a3c91c | 941 | |
82a04857 FG |
942 | if ($prepare) { |
943 | my $repl_conf = PVE::ReplicationConfig->new(); | |
944 | if ($repl_conf->check_for_existing_jobs($vmid, 1)) { | |
945 | # remove all replication snapshots | |
946 | my $volumes = $class->get_replicatable_volumes($storecfg, $vmid, $conf, 1); | |
947 | my $sorted_volids = [ sort keys %$volumes ]; | |
948 | ||
949 | # remove all local replication snapshots (jobid => undef) | |
950 | my $logfunc = sub { my $line = shift; chomp $line; print "$line\n"; }; | |
951 | PVE::Replication::prepare($storecfg, $sorted_volids, undef, 0, undef, $logfunc); | |
952 | } | |
953 | ||
954 | $class->foreach_volume($snap, sub { | |
955 | my ($vs, $volume) = @_; | |
58a3c91c | 956 | |
82a04857 FG |
957 | $class->__snapshot_rollback_vol_possible($volume, $snapname); |
958 | }); | |
959 | } | |
58a3c91c FG |
960 | |
961 | die "unable to rollback to incomplete snapshot (snapstate = $snap->{snapstate})\n" | |
962 | if $snap->{snapstate}; | |
963 | ||
964 | if ($prepare) { | |
965 | $class->check_lock($conf); | |
966 | $class->__snapshot_rollback_vm_stop($vmid); | |
967 | } | |
968 | ||
969 | die "unable to rollback vm $vmid: vm is running\n" | |
970 | if $class->__snapshot_check_running($vmid); | |
971 | ||
972 | if ($prepare) { | |
973 | $conf->{lock} = 'rollback'; | |
974 | } else { | |
975 | die "got wrong lock\n" if !($conf->{lock} && $conf->{lock} eq 'rollback'); | |
976 | delete $conf->{lock}; | |
58a3c91c | 977 | |
58a3c91c FG |
978 | my $unused = $class->__snapshot_rollback_get_unused($conf, $snap); |
979 | ||
980 | foreach my $volid (@$unused) { | |
981 | $class->add_unused_volume($conf, $volid); | |
982 | } | |
983 | ||
58a3c91c FG |
984 | # copy snapshot config to current config |
985 | $conf = $class->__snapshot_apply_config($conf, $snap); | |
986 | $conf->{parent} = $snapname; | |
58a3c91c FG |
987 | } |
988 | ||
3fa4ce45 DC |
989 | $class->__snapshot_rollback_hook($vmid, $conf, $snap, $prepare, $data); |
990 | ||
58a3c91c FG |
991 | $class->write_config($vmid, $conf); |
992 | ||
993 | if (!$prepare && $snap->{vmstate}) { | |
3fa4ce45 | 994 | $class->__snapshot_rollback_vm_start($vmid, $snap->{vmstate}, $data); |
58a3c91c FG |
995 | } |
996 | }; | |
997 | ||
998 | $class->lock_config($vmid, $updatefn); | |
999 | ||
261d47a4 | 1000 | $class->foreach_volume($snap, sub { |
58a3c91c FG |
1001 | my ($vs, $volume) = @_; |
1002 | ||
1003 | $class->__snapshot_rollback_vol_rollback($volume, $snapname); | |
1004 | }); | |
1005 | ||
1006 | $prepare = 0; | |
1007 | $class->lock_config($vmid, $updatefn); | |
1008 | } | |
1009 | ||
eb50bb61 RV |
1010 | # bash completion helper |
1011 | ||
90fba9cd WB |
1012 | sub snapshot_list { |
1013 | my ($class, $vmid) = @_; | |
eb50bb61 | 1014 | |
90fba9cd WB |
1015 | my $snapshot = eval { |
1016 | my $conf = $class->load_config($vmid); | |
1017 | my $snapshots = $conf->{snapshots} || []; | |
1018 | [ sort keys %$snapshots ] | |
1019 | } || []; | |
eb50bb61 RV |
1020 | |
1021 | return $snapshot; | |
1022 | } | |
1023 | ||
58a3c91c | 1024 | 1; |