]> git.proxmox.com Git - pve-manager.git/blob - PVE/OpenVZMigrate.pm
b0c66e642aaeb0e085e225ff5492b1d9355ed409
[pve-manager.git] / PVE / OpenVZMigrate.pm
1 package PVE::OpenVZMigrate;
2
3 use strict;
4 use warnings;
5 use PVE::AbstractMigrate;
6 use File::Basename;
7 use File::Copy;
8 use PVE::Tools;
9 use PVE::INotify;
10 use PVE::Cluster;
11 use PVE::Storage;
12 use PVE::OpenVZ;
13
14 use base qw(PVE::AbstractMigrate);
15
16 # fixme: lock VM on target node
17
18 sub lock_vm {
19 my ($self, $vmid, $code, @param) = @_;
20
21 return PVE::OpenVZ::lock_container($vmid, undef, $code, @param);
22 }
23
24 sub prepare {
25 my ($self, $vmid) = @_;
26
27 my $online = $self->{opts}->{online};
28
29 $self->{storecfg} = PVE::Storage::config();
30 $self->{vzconf} = PVE::OpenVZ::read_global_vz_config(),
31
32 # test is VM exist
33 my $conf = $self->{vmconf} = PVE::OpenVZ::load_config($vmid);
34
35 my $path = PVE::OpenVZ::get_privatedir($conf, $vmid);
36 my ($vtype, $volid) = PVE::Storage::path_to_volume_id($self->{storecfg}, $path);
37 my ($storage, $volname) = PVE::Storage::parse_volume_id($volid, 1) if $volid;
38
39 die "can't determine assigned storage\n" if !$storage;
40
41 # check if storage is available on both nodes
42 my $scfg = PVE::Storage::storage_check_node($self->{storecfg}, $storage);
43 PVE::Storage::storage_check_node($self->{storecfg}, $storage, $self->{node});
44
45 # we simply use the backup dir to store temporary dump files
46 # Note: this is on shared storage if the storage is 'shared'
47 $self->{dumpdir} = PVE::Storage::get_backup_dir($self->{storecfg}, $storage);
48
49 PVE::Storage::activate_volumes($self->{storecfg}, [ $volid ]);
50
51 $self->{storage} = $storage;
52 $self->{privatedir} = $path;
53
54 $self->{rootdir} = PVE::OpenVZ::get_rootdir($conf, $vmid);
55
56 $self->{shared} = $scfg->{shared};
57
58 my $running = 0;
59 if (PVE::OpenVZ::check_running($vmid)) {
60 die "cant migrate running container without --online\n" if !$online;
61 $running = 1;
62 }
63
64 # fixme: test if VM uses local resources
65
66 # test ssh connection
67 my $cmd = [ @{$self->{rem_ssh}}, '/bin/true' ];
68 eval { $self->cmd_quiet($cmd); };
69 die "Can't connect to destination address using public key\n" if $@;
70
71 if ($running) {
72
73 # test if OpenVZ is running
74 $cmd = [ @{$self->{rem_ssh}}, '/etc/init.d/vz status' ];
75 eval { $self->cmd_quiet($cmd); };
76 die "OpenVZ is not running on the target machine\n" if $@;
77
78 # test if CPT modules are loaded for online migration
79 die "vzcpt module is not loaded\n" if ! -f '/proc/cpt';
80
81 $cmd = [ @{$self->{rem_ssh}}, 'test -f /proc/rst' ];
82 eval { $self->cmd_quiet($cmd); };
83 die "vzrst module is not loaded on the target machine\n" if $@;
84 }
85
86 # fixme: do we want to test if IPs exists on target node?
87
88 return $running;
89 }
90
91 sub phase1 {
92 my ($self, $vmid) = @_;
93
94 $self->log('info', "starting migration of CT $self->{vmid} to node '$self->{node}' ($self->{nodeip})");
95
96 my $conf = $self->{vmconf};
97
98 if ($self->{running}) {
99 $self->log('info', "container is running - using online migration");
100 }
101
102 my $cmd = [ @{$self->{rem_ssh}}, 'mkdir', '-p', $self->{rootdir} ];
103 $self->cmd_quiet($cmd, errmsg => "Failed to make container root directory");
104
105 my $privatedir = $self->{privatedir};
106
107 if (!$self->{shared}) {
108
109 $cmd = [ @{$self->{rem_ssh}}, 'mkdir', '-p', $privatedir ];
110 $self->cmd_quiet($cmd, errmsg => "Failed to make container private directory");
111
112 $self->{undo_private} = $privatedir;
113
114 $self->log('info', "starting rsync phase 1");
115 my $basedir = dirname($privatedir);
116 $cmd = [ @{$self->{rsync_cmd}}, '--sparse', $privatedir, "root\@$self->{nodeip}:$basedir" ];
117 $self->cmd($cmd, errmsg => "Failed to sync container private area");
118 } else {
119 $self->log('info', "container data is on shared storage '$self->{storage}'");
120 }
121
122 my $conffile = PVE::OpenVZ::config_file($vmid);
123 my $newconffile = PVE::OpenVZ::config_file($vmid, $self->{node});
124
125 my $srccfgdir = dirname($conffile);
126 my $newcfgdir = dirname($newconffile);
127 foreach my $s (PVE::OpenVZ::SCRIPT_EXT) {
128 my $scriptfn = "${vmid}.$s";
129 my $srcfn = "$srccfgdir/$scriptfn";
130 next if ! -f $srcfn;
131 my $dstfn = "$newcfgdir/$scriptfn";
132 copy($srcfn, $dstfn) || die "copy '$srcfn' to '$dstfn' failed - $!\n";
133 }
134
135 if ($self->{running}) {
136 # fixme: save state and quota
137 $self->log('info', "start live migration - suspending container");
138 $cmd = [ 'vzctl', '--skiplock', 'chkpnt', $vmid, '--suspend' ];
139 $self->cmd_quiet($cmd, errmsg => "Failed to suspend container");
140
141 $self->{undo_suspend} = 1;
142
143 $self->log('info', "dump container state");
144 $self->{dumpfile} = "$self->{dumpdir}/dump.$vmid";
145 $cmd = [ 'vzctl', '--skiplock', 'chkpnt', $vmid, '--dump', '--dumpfile', $self->{dumpfile} ];
146 $self->cmd_quiet($cmd, errmsg => "Failed to dump container state");
147
148 if (!$self->{shared}) {
149 $self->log('info', "copy dump file to target node");
150 $self->{undo_copy_dump} = 1;
151 $cmd = [ @{$self->{scp_cmd}}, $self->{dumpfile}, "root\@$self->{nodeip}:$self->{dumpfile}"];
152 $self->cmd_quiet($cmd, errmsg => "Failed to copy dump file");
153
154 $self->log('info', "starting rsync (2nd pass)");
155 my $basedir = dirname($privatedir);
156 $cmd = [ @{$self->{rsync_cmd}}, $privatedir, "root\@$self->{nodeip}:$basedir" ];
157 $self->cmd($cmd, errmsg => "Failed to sync container private area");
158 }
159 } else {
160 if (PVE::OpenVZ::check_mounted($conf, $vmid)) {
161 $self->log('info', "unmounting container");
162 $cmd = [ 'vzctl', '--skiplock', 'umount', $vmid ];
163 $self->cmd_quiet($cmd, errmsg => "Failed to umount container");
164 }
165 }
166
167 my $disk_quota = $conf->{disk_quota}->{value};
168 if (!defined($disk_quota) || ($disk_quota != 0)) {
169 $disk_quota = $self->{disk_quota} = 1;
170
171 $self->log('info', "dump 2nd level quota");
172 $self->{quotadumpfile} = "$self->{dumpdir}/quotadump.$vmid";
173 $cmd = "vzdqdump $vmid -U -G -T > " . PVE::Tools::shellquote($self->{quotadumpfile});
174 $self->cmd_quiet($cmd, errmsg => "Failed to dump 2nd level quota");
175
176 if (!$self->{shared}) {
177 $self->log('info', "copy 2nd level quota to target node");
178 $self->{undo_copy_quota_dump} = 1;
179 $cmd = [@{$self->{scp_cmd}}, $self->{quotadumpfile},
180 "root\@$self->{nodeip}:$self->{quotadumpfile}"];
181 $self->cmd_quiet($cmd, errmsg => "Failed to copy 2nd level quota dump");
182 }
183 }
184
185 # everythin copied - make sure container is stoped
186 # fixme_ do we need to start on the other node first?
187 if ($self->{running}) {
188 delete $self->{undo_suspend};
189 $cmd = [ 'vzctl', '--skiplock', 'chkpnt', $vmid, '--kill' ];
190 $self->cmd_quiet($cmd, errmsg => "Failed to kill container");
191 $cmd = [ 'vzctl', '--skiplock', 'umount', $vmid ];
192 sleep(1); # hack: wait - else there are open files
193 $self->cmd_quiet($cmd, errmsg => "Failed to umount container");
194 }
195
196 # move config
197 die "Failed to move config to node '$self->{node}' - rename failed: $!\n"
198 if !rename($conffile, $newconffile);
199 }
200
201 sub phase1_cleanup {
202 my ($self, $vmid, $err) = @_;
203
204 $self->log('info', "aborting phase 1 - cleanup resources");
205
206 my $conf = $self->{vmconf};
207
208 if ($self->{undo_suspend}) {
209 my $cmd = [ 'vzctl', '--skiplock', 'chkpnt', $vmid, '--resume' ];
210 $self->cmd_logerr($cmd, errmsg => "Failed to resume container");
211 }
212
213 if ($self->{undo_private}) {
214 $self->log('info', "removing copied files on target node");
215 my $cmd = [ @{$self->{rem_ssh}}, 'rm', '-rf', $self->{undo_private} ];
216 $self->cmd_logerr($cmd, errmsg => "Failed to remove copied files");
217 }
218
219 # fixme: that seem to be very dangerous and not needed
220 #my $cmd = [ @{$self->{rem_ssh}}, 'rm', '-rf', $self->{rootdir} ];
221 #eval { $self->cmd_quiet($cmd); };
222
223 my $newconffile = PVE::OpenVZ::config_file($vmid, $self->{node});
224 my $newcfgdir = dirname($newconffile);
225 foreach my $s (PVE::OpenVZ::SCRIPT_EXT) {
226 my $scriptfn = "${vmid}.$s";
227 my $dstfn = "$newcfgdir/$scriptfn";
228 if (-f $dstfn) {
229 $self->log('err', "unlink '$dstfn' failed - $!") if !unlink $dstfn;
230 }
231 }
232 }
233
234 sub init_target_vm {
235 my ($self, $vmid) = @_;
236
237 my $conf = $self->{vmconf};
238
239 $self->log('info', "initialize container on remote node '$self->{node}'");
240
241 my $cmd = [ @{$self->{rem_ssh}}, 'vzctl', '--quiet', 'set', $vmid,
242 '--applyconfig_map', 'name', '--save' ];
243
244 $self->cmd_quiet($cmd, errmsg => "Failed to apply config on target node");
245
246 if ($self->{disk_quota}) {
247 $self->log('info', "initializing remote quota");
248 $cmd = [ @{$self->{rem_ssh}}, 'vzctl', 'quotainit', $vmid];
249 $self->cmd_quiet($cmd, errmsg => "Failed to initialize quota");
250 $self->log('info', "turn on remote quota");
251 $cmd = [ @{$self->{rem_ssh}}, 'vzctl', 'quotaon', $vmid];
252 $self->cmd_quiet($cmd, errmsg => "Failed to turn on quota");
253 $self->log('info', "load 2nd level quota");
254 $cmd = [ @{$self->{rem_ssh}}, "(vzdqload $vmid -U -G -T < " .
255 PVE::Tools::shellquote($self->{quotadumpfile}) .
256 " && vzquota reload2 $vmid)"];
257 $self->cmd_quiet($cmd, errmsg => "Failed to load 2nd level quota");
258 if (!$self->{running}) {
259 $self->log('info', "turn off remote quota");
260 $cmd = [ @{$self->{rem_ssh}}, 'vzquota', 'off', $vmid];
261 $self->cmd_quiet($cmd, errmsg => "Failed to turn off quota");
262 }
263 }
264 }
265
266 sub phase2 {
267 my ($self, $vmid) = @_;
268
269 my $conf = $self->{vmconf};
270
271 $self->{target_initialized} = 1;
272 init_target_vm($self, $vmid);
273
274 $self->log('info', "starting container on remote node '$self->{node}'");
275
276 $self->log('info', "restore container state");
277 $self->{dumpfile} = "$self->{dumpdir}/dump.$vmid";
278 my $cmd = [ @{$self->{rem_ssh}}, 'vzctl', 'restore', $vmid, '--undump',
279 '--dumpfile', $self->{dumpfile}, '--skip_arpdetect' ];
280 $self->cmd_quiet($cmd, errmsg => "Failed to restore container");
281
282 $cmd = [ @{$self->{rem_ssh}}, 'vzctl', 'restore', $vmid, '--resume' ];
283 $self->cmd_quiet($cmd, errmsg => "Failed to resume container");
284 }
285
286 sub phase3 {
287 my ($self, $vmid) = @_;
288
289 if (!$self->{target_initialized}) {
290 init_target_vm($self, $vmid);
291 }
292
293 }
294
295 sub phase3_cleanup {
296 my ($self, $vmid, $err) = @_;
297
298 my $conf = $self->{vmconf};
299
300 if (!$self->{shared}) {
301 # destroy local container data
302 $self->log('info', "removing container files on local node");
303 my $cmd = [ 'rm', '-rf', $self->{privatedir} ];
304 $self->cmd_logerr($cmd);
305 }
306
307 if ($self->{disk_quota}) {
308 my $cmd = [ 'vzquota', 'drop', $vmid];
309 $self->cmd_logerr($cmd, errmsg => "Failed to drop local quota");
310 }
311 }
312
313 sub final_cleanup {
314 my ($self, $vmid) = @_;
315
316 $self->log('info', "start final cleanup");
317
318 my $conf = $self->{vmconf};
319
320 unlink($self->{quotadumpfile}) if $self->{quotadumpfile};
321
322 unlink($self->{dumpfile}) if $self->{dumpfile};
323
324 if ($self->{undo_copy_dump} && $self->{dumpfile}) {
325 my $cmd = [ @{$self->{rem_ssh}}, 'rm', '-f', $self->{dumpfile} ];
326 $self->cmd_logerr($cmd, errmsg => "Failed to remove dump file");
327 }
328
329 if ($self->{undo_copy_quota_dump} && $self->{quotadumpfile}) {
330 my $cmd = [ @{$self->{rem_ssh}}, 'rm', '-f', $self->{quotadumpfile} ];
331 $self->cmd_logerr($cmd, errmsg => "Failed to remove 2nd level quota dump file");
332 }
333 }
334
335 1;