]>
Commit | Line | Data |
---|---|---|
1e3baf05 DM |
1 | #!/usr/bin/perl -w |
2 | # | |
3 | # Copyright (C) 2007-2009 Proxmox Server Solutions GmbH | |
4 | # | |
5 | # Copyright: vzdump is under GNU GPL, the GNU General Public License. | |
6 | # | |
7 | # This program is free software; you can redistribute it and/or modify | |
8 | # it under the terms of the GNU General Public License as published by | |
9 | # the Free Software Foundation; version 2 dated June, 1991. | |
10 | # | |
11 | # This program is distributed in the hope that it will be useful, | |
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | # GNU General Public License for more details. | |
15 | # | |
16 | # You should have received a copy of the GNU General Public License | |
17 | # along with this program; if not, write to the | |
18 | # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, | |
19 | # MA 02110-1301, USA. | |
20 | # | |
21 | # Author: Dietmar Maurer <dietmar@proxmox.com> | |
22 | # | |
23 | ||
24 | use strict; | |
25 | use Getopt::Long; | |
26 | use Sys::Syslog; | |
27 | use File::Path; | |
28 | use PVE::VZDump; | |
29 | use PVE::Storage; | |
30 | ||
31 | $ENV{LANG} = "C"; # avoid locale related issues/warnings | |
32 | ||
33 | openlog ('vzdump', 'cons,pid', 'daemon'); | |
34 | ||
35 | my @std_opts = ('extract', 'storage=s', 'info', 'prealloc', 'unique'); | |
36 | ||
37 | sub print_usage { | |
38 | my $msg = shift; | |
39 | ||
40 | print STDERR "ERROR: $msg\n\n" if $msg; | |
41 | ||
42 | print STDERR "usage: $0 [OPTIONS] <ARCHIVE> <VMID>\n\n"; | |
43 | } | |
44 | ||
45 | sub shellquote { | |
46 | my $str = shift; | |
47 | ||
48 | return "''" if !defined ($str) || ($str eq ''); | |
49 | ||
50 | die "unable to quote string containing null (\\000) bytes" | |
51 | if $str =~ m/\x00/; | |
52 | ||
53 | # from String::ShellQuote | |
54 | if ($str =~ m|[^\w!%+,\-./:@^]|) { | |
55 | ||
56 | # ' -> '\'' | |
57 | $str =~ s/'/'\\''/g; | |
58 | ||
59 | $str = "'$str'"; | |
60 | $str =~ s/^''//; | |
61 | $str =~ s/''$//; | |
62 | } | |
63 | ||
64 | return $str; | |
65 | } | |
66 | ||
67 | my $quoted_cmd = shellquote ($0); | |
68 | foreach my $arg (@ARGV) { | |
69 | $quoted_cmd .= " " . shellquote ($arg); | |
70 | } | |
71 | ||
72 | my $opts = {}; | |
73 | if (!GetOptions ($opts, @std_opts)) { | |
74 | print_usage (); | |
75 | exit (-1); | |
76 | } | |
77 | ||
78 | if ($#ARGV != 1) { | |
79 | print_usage (); | |
80 | exit (-1); | |
81 | } | |
82 | ||
83 | my $archive = shift; | |
84 | my $vmid = PVE::VZDump::check_vmids ((shift))->[0]; | |
85 | ||
86 | $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub { | |
87 | die "interrupted by signal\n"; | |
88 | }; | |
89 | ||
90 | sub debugmsg { PVE::VZDump::debugmsg (@_); } # just a shortcut | |
91 | ||
92 | sub run_command { PVE::VZDump::run_command (undef, @_); } # just a shortcut | |
93 | ||
94 | if ($opts->{extract}) { | |
95 | ||
96 | # NOTE: this is run as tar subprocess (--to-command) | |
97 | ||
98 | my $filename = $ENV{TAR_FILENAME}; | |
99 | die "got strange environment - no TAR_FILENAME\n" if !$filename; | |
100 | ||
101 | my $filesize = $ENV{TAR_SIZE}; | |
102 | die "got strange file size '$filesize'\n" if !$filesize; | |
103 | ||
104 | my $tmpdir = $ENV{VZDUMP_TMPDIR}; | |
105 | die "got strange environment - no VZDUMP_TMPDIR\n" if !$tmpdir; | |
106 | ||
107 | my $filetype = $ENV{TAR_FILETYPE} || 'none'; | |
108 | die "got strange filetype '$filetype'\n" if $filetype ne 'f'; | |
109 | ||
110 | my $conffile = "$tmpdir/qemu-server.conf"; | |
111 | my $statfile = "$tmpdir/qmrestore.stat"; | |
112 | ||
113 | if ($opts->{info}) { | |
114 | print STDERR "reading archive member '$filename'\n"; | |
115 | } else { | |
116 | print STDERR "extracting '$filename' from archive\n"; | |
117 | } | |
118 | ||
119 | if ($filename eq 'qemu-server.conf') { | |
120 | my $outfd = IO::File->new ($conffile, "w") || | |
121 | die "unable to write file '$conffile'\n"; | |
122 | ||
123 | while (defined (my $line = <>)) { | |
124 | print $outfd $line; | |
125 | print STDERR "CONFIG: $line" if $opts->{info}; | |
126 | } | |
127 | ||
128 | $outfd->close(); | |
129 | ||
130 | exit (0); | |
131 | } | |
132 | ||
133 | if ($opts->{info}) { | |
134 | exec 'dd', 'bs=256K', "of=/dev/null"; | |
135 | die "couldn't exec dd: $!\n"; | |
136 | } | |
137 | ||
138 | my $conffd = IO::File->new ($conffile, "r") || | |
139 | die "unable to read file '$conffile'\n"; | |
140 | ||
141 | my $map; | |
142 | while (defined (my $line = <$conffd>)) { | |
143 | if ($line =~ m/^\#vzdump\#map:(\S+):(\S+):(\d+):(\S*):$/) { | |
144 | $map->{$2} = { virtdev => $1, size => $3, storeid => $4 }; | |
145 | } | |
146 | } | |
147 | close ($conffd); | |
148 | ||
149 | my $statfd = IO::File->new ($statfile, "a") || | |
150 | die "unable to open file '$statfile'\n"; | |
151 | ||
152 | if ($filename !~ m/^.*\.([^\.]+)$/){ | |
153 | die "got strange filename '$filename'\n"; | |
154 | } | |
155 | my $format = $1; | |
156 | ||
157 | my $path; | |
158 | ||
159 | if (!$map) { | |
160 | print STDERR "restoring old style vzdump archive - " . | |
161 | "no device map inside archive\n"; | |
162 | die "can't restore old style archive to storage '$opts->{storage}'\n" | |
163 | if $opts->{storage} && $opts->{storage} ne 'local'; | |
164 | ||
165 | my $dir = "/var/lib/vz/images/$vmid"; | |
166 | mkpath $dir; | |
167 | ||
168 | $path = "$dir/$filename"; | |
169 | ||
170 | print $statfd "vzdump::$path\n"; | |
171 | $statfd->close(); | |
172 | ||
173 | } else { | |
174 | ||
175 | my $info = $map->{$filename}; | |
176 | die "no vzdump info for '$filename'\n" if !$info; | |
177 | ||
178 | if ($filename !~ m/^vm-disk-$info->{virtdev}\.([^\.]+)$/){ | |
179 | die "got strange filename '$filename'\n"; | |
180 | } | |
181 | ||
182 | if ($filesize != $info->{size}) { | |
183 | die "detected size difference for '$filename' " . | |
184 | "($filesize != $info->{size})\n"; | |
185 | } | |
186 | ||
187 | my $storeid; | |
188 | if ($opts->{storage}) { | |
189 | $storeid = $opts->{storage}; | |
190 | } else { | |
191 | $storeid = $info->{storeid} || 'local'; | |
192 | } | |
193 | ||
194 | my $cfg = PVE::Storage::load_config(); | |
195 | my $scfg = PVE::Storage::storage_config ($cfg, $storeid); | |
196 | ||
197 | my $alloc_size = ($filesize + 1024 - 1)/1024; | |
198 | if ($scfg->{type} eq 'dir' || $scfg->{type} eq 'nfs') { | |
199 | # hack: we just alloc a small file (32K) - we overwrite it anyways | |
200 | $alloc_size = 32; | |
201 | } else { | |
202 | die "unable to restore '$filename' to storage '$storeid'\n" . | |
203 | "storage type '$scfg->{type}' does not support format '$format\n" | |
204 | if $format ne 'raw'; | |
205 | } | |
206 | ||
207 | my $volid = PVE::Storage::vdisk_alloc ($cfg, $storeid, $vmid, | |
208 | $format, undef, $alloc_size); | |
209 | ||
210 | print STDERR "new volume ID is '$volid'\n"; | |
211 | ||
212 | print $statfd "vzdump:$info->{virtdev}:$volid\n"; | |
213 | $statfd->close(); | |
214 | ||
215 | $path = PVE::Storage::path ($cfg, $volid); | |
216 | } | |
217 | ||
218 | print STDERR "restore data to '$path' ($filesize bytes)\n"; | |
219 | ||
220 | if ($opts->{prealloc} || $format ne 'raw' || (-b $path)) { | |
221 | exec 'dd', 'bs=256K', "of=$path"; | |
222 | die "couldn't exec dd: $!\n"; | |
223 | } else { | |
224 | exec '/usr/lib/qemu-server/sparsecp', $path; | |
225 | die "couldn't exec sparsecp: $!\n"; | |
226 | } | |
227 | } | |
228 | ||
229 | sub restore_cleanup { | |
230 | my $statfile = shift; | |
231 | ||
232 | return if $opts->{info}; | |
233 | ||
234 | debugmsg ('info', "starting cleanup"); | |
235 | if (my $fd = IO::File->new ($statfile, "r")) { | |
236 | while (defined (my $line = <$fd>)) { | |
237 | if ($line =~ m/vzdump:([^\s:]*):(\S+)$/) { | |
238 | my $volid = $2; | |
239 | eval { | |
240 | if ($volid =~ m|^/|) { | |
241 | unlink $volid || die 'unlink failed\n'; | |
242 | } else { | |
243 | my $cfg = PVE::Storage::load_config(); | |
244 | PVE::Storage::vdisk_free ($cfg, $volid); | |
245 | } | |
246 | debugmsg ('info', "temporary volume '$volid' sucessfuly removed"); | |
247 | }; | |
248 | debugmsg ('err', "unable to cleanup '$volid' - $@") if $@; | |
249 | } else { | |
250 | debugmsg ('info', "unable to parse line in statfile - $line"); | |
251 | } | |
252 | } | |
253 | $fd->close(); | |
254 | } | |
255 | } | |
256 | ||
257 | sub restore_qemu { | |
258 | my ($archive, $vmid, $tmpdir) = @_; | |
259 | ||
260 | local $ENV{VZDUMP_TMPDIR} = $tmpdir; | |
261 | ||
262 | my $subcmd = shellquote ("--to-command=${quoted_cmd}\ --extract"); | |
263 | my $cmd = "tar xf '$archive' $subcmd"; | |
264 | run_command ($cmd); | |
265 | ||
266 | return if $opts->{info}; | |
267 | ||
268 | # reed new mapping | |
269 | my $map = {}; | |
270 | my $statfile = "$tmpdir/qmrestore.stat"; | |
271 | if (my $fd = IO::File->new ($statfile, "r")) { | |
272 | while (defined (my $line = <$fd>)) { | |
273 | if ($line =~ m/vzdump:([^\s:]*):(\S+)$/) { | |
274 | $map->{$1} = $2 if $1; | |
275 | } else { | |
276 | debugmsg ('info', "unable to parse line in statfile - $line"); | |
277 | } | |
278 | } | |
279 | $fd->close(); | |
280 | } | |
281 | ||
282 | my $confsrc = "$tmpdir/qemu-server.conf"; | |
283 | ||
284 | my $srcfd = new IO::File ($confsrc, "r") || | |
285 | die "unable to open file '$confsrc'\n"; | |
286 | ||
287 | my $conffile = PVE::QemuServer::config_file ($vmid); | |
288 | my $tmpfn = "$conffile.$$.tmp"; | |
289 | ||
290 | my $outfd = new IO::File ($tmpfn, "w") || | |
291 | die "unable to write config for VM $vmid\n"; | |
292 | ||
293 | eval { | |
294 | while (defined (my $line = <$srcfd>)) { | |
295 | next if $line =~ m/^\#vzdump\#/; | |
296 | next if $line =~ m/^lock:/; | |
297 | ||
298 | if (($line =~ m/^((vlan)\d+):(.*)$/) && ($opts->{unique})) { | |
299 | my ($id,$ethcfg) = ($1,$3); | |
300 | $ethcfg =~ s/^\s+//; | |
301 | my ($model, $mac) = split(/\=/,$ethcfg); | |
302 | my $printvlan = PVE::QemuServer::print_vlan (PVE::QemuServer::parse_vlan ($model)); | |
303 | print $outfd "$id: $printvlan\n"; | |
304 | } elsif ($line =~ m/^((ide|scsi|virtio)\d+):(.*)$/) { | |
305 | my $virtdev = $1; | |
306 | my $value = $2; | |
307 | if ($line =~ m/backup=no/) { | |
308 | print $outfd "#$line"; | |
309 | } elsif ($virtdev && $map->{$virtdev}) { | |
310 | my $di = PVE::QemuServer::parse_drive ($virtdev, $value); | |
311 | $di->{file} = $map->{$virtdev}; | |
312 | $value = PVE::QemuServer::print_drive ($vmid, $di); | |
313 | print $outfd "$virtdev: $value\n"; | |
314 | } else { | |
315 | print $outfd $line; | |
316 | } | |
317 | } else { | |
318 | print $outfd $line; | |
319 | } | |
320 | } | |
321 | }; | |
322 | my $err = $@; | |
323 | ||
324 | $outfd->close(); | |
325 | ||
326 | if ($err) { | |
327 | unlink $tmpfn; | |
328 | die $err; | |
329 | } else { | |
330 | rename $tmpfn, $conffile; | |
331 | } | |
332 | } | |
333 | ||
334 | my $firstfile = PVE::VZDump::read_firstfile ($archive); | |
335 | if ($firstfile ne 'qemu-server.conf') { | |
336 | die "ERROR: file '$archive' dos not lock like a QemuServer vzdump backup\n"; | |
337 | } | |
338 | ||
339 | my $tmpdir = "/var/tmp/vzdumptmp$$"; | |
340 | ||
341 | PVE::QemuServer::lock_config ($vmid, sub { | |
342 | ||
343 | my $conffile = PVE::QemuServer::config_file ($vmid); | |
344 | ||
345 | die "unable to restore VM '$vmid' - VM already exists\n" | |
346 | if -f $conffile; | |
347 | ||
348 | mkpath $tmpdir; | |
349 | ||
350 | eval { | |
351 | debugmsg ('info', "restore QemuServer backup '$archive' " . | |
352 | "using ID $vmid", undef, 1) if !$opts->{info}; | |
353 | ||
354 | restore_qemu ($archive, $vmid, $tmpdir); | |
355 | ||
356 | if ($opts->{info}) { | |
357 | debugmsg ('info', "reading '$archive' successful"); | |
358 | } else { | |
359 | debugmsg ('info', "restore QemuServer backup '$archive' successful", | |
360 | undef, 1); | |
361 | } | |
362 | }; | |
363 | my $err = $@; | |
364 | ||
365 | if ($err) { | |
366 | local $SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = sub { | |
367 | debugmsg ('info', "got interrupt - ignored (cleanup phase)"); | |
368 | }; | |
369 | ||
370 | restore_cleanup ("$tmpdir/qmrestore.stat") if $err; | |
371 | } | |
372 | ||
373 | die $err if $err; | |
374 | }); | |
375 | ||
376 | my $err = $@; | |
377 | ||
378 | rmtree $tmpdir; | |
379 | ||
380 | if ($err) { | |
381 | if ($opts->{info}) { | |
382 | debugmsg ('info', "reading '$archive' failed - $err"); | |
383 | } else { | |
384 | ||
385 | debugmsg ('err', "restore QemuServer backup '$archive' failed - $err", | |
386 | undef, 1); | |
387 | } | |
388 | exit (-1); | |
389 | } | |
390 | ||
391 | exit (0); | |
392 | ||
393 | __END__ | |
394 | ||
395 | =head1 NAME | |
396 | ||
397 | qmrestore - restore QemuServer vzdump backups | |
398 | ||
399 | =head1 SYNOPSIS | |
400 | ||
401 | qmrestore [OPTIONS] <archive> <VMID> | |
402 | ||
403 | --info read/verify archive and print relevant | |
404 | information (test run) | |
405 | ||
406 | --unique assign a unique random ethernet address | |
407 | ||
408 | --storage <STORAGE_ID> restore to storage <STORAGE_ID> | |
409 | ||
410 | --prealloc never generate sparse files | |
411 | ||
412 | =head1 DESCRIPTION | |
413 | ||
414 | Restore the QemuServer vzdump backup <archive> to virtual machine | |
415 | <VMID>. Volumes are allocated on the original storage if there is no | |
416 | C<--storage> specified. | |
417 | ||
418 | =head1 SEE ALSO | |
419 | ||
420 | vzdump(1) vzrestore(1) |